@aiao/rxdb-angular 0.0.2 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/eslint.config.mjs +58 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +13 -23
  4. package/project.json +48 -0
  5. package/src/InfiniteScrollingList.ts +123 -0
  6. package/src/devtools/devtools.html +99 -0
  7. package/src/devtools/devtools.interface.ts +3 -0
  8. package/src/devtools/devtools.scss +49 -0
  9. package/src/devtools/devtools.spec.ts +30 -0
  10. package/src/devtools/devtools.ts +207 -0
  11. package/src/devtools/entity-table-item.ts +47 -0
  12. package/src/devtools/entity-table-tools.html +56 -0
  13. package/src/devtools/entity-table-tools.scss +8 -0
  14. package/src/devtools/entity-table-tools.ts +153 -0
  15. package/src/devtools/event-tools.html +15 -0
  16. package/src/devtools/event-tools.scss +18 -0
  17. package/src/devtools/event-tools.ts +45 -0
  18. package/src/devtools/scroll-advanced-calc.service.ts +41 -0
  19. package/src/devtools/settings.html +46 -0
  20. package/src/devtools/settings.scss +19 -0
  21. package/src/devtools/settings.ts +122 -0
  22. package/src/hooks.ts +307 -0
  23. package/src/index.ts +7 -0
  24. package/src/rxdb-change-detector.directive.spec.ts +94 -0
  25. package/src/rxdb-change-detector.directive.ts +35 -0
  26. package/src/rxdb.provider.ts +13 -0
  27. package/src/rxdb.service.spec.ts +31 -0
  28. package/src/rxdb.service.ts +35 -0
  29. package/src/test-setup.ts +14 -0
  30. package/src/use-action.spec.ts +88 -0
  31. package/src/use-action.ts +20 -0
  32. package/src/use-state.spec.ts +105 -0
  33. package/src/use-state.ts +28 -0
  34. package/tsconfig.json +33 -0
  35. package/tsconfig.lib.json +42 -0
  36. package/tsconfig.lib.prod.json +10 -0
  37. package/tsconfig.spec.json +23 -0
  38. package/vite.config.mts +55 -0
  39. package/fesm2022/aiao-rxdb-angular.mjs +0 -47
  40. package/fesm2022/aiao-rxdb-angular.mjs.map +0 -1
  41. package/index.d.ts +0 -15
package/src/hooks.ts ADDED
@@ -0,0 +1,307 @@
1
+ import { EntityStaticType, EntityType, TreeEntityType } from '@aiao/rxdb';
2
+ import { GraphEntityType } from '@aiao/rxdb-plugin-graph';
3
+ import { isFunction } from '@aiao/utils';
4
+ import { effect, Signal, signal } from '@angular/core';
5
+ import { toLazySignal } from 'ngxtension/to-lazy-signal';
6
+ import { Observable, Subscriber, Subscription } from 'rxjs';
7
+
8
+ type UseOptions<T> = T | (() => T);
9
+
10
+ export interface RxDBResource<T> {
11
+ /**
12
+ * 资源的值
13
+ */
14
+ readonly value: Signal<T>;
15
+ /**
16
+ * 资源的错误
17
+ */
18
+ readonly error: Signal<Error | undefined>;
19
+ /**
20
+ * 资源的加载状态
21
+ */
22
+ readonly isLoading: Signal<boolean>;
23
+ /**
24
+ * 资源是否为空
25
+ */
26
+ readonly isEmpty: Signal<boolean | undefined>;
27
+ /**
28
+ * 资源是否有值
29
+ */
30
+ readonly hasValue: Signal<boolean>;
31
+ }
32
+
33
+ export const useRepositoryQuery = <T extends EntityType, RT>(
34
+ EntityType: T,
35
+ method: string,
36
+ defaultValue: any,
37
+ options: UseOptions<any>
38
+ ): RxDBResource<RT> => {
39
+ let _observer: Subscriber<any>;
40
+ let sub: Subscription;
41
+
42
+ const _has_sub = signal<boolean>(false);
43
+ const _refresh = signal<number>(0);
44
+
45
+ const _eff = effect(() => {
46
+ if (_has_sub() === false) return;
47
+ // re-run when refresh is called or options change
48
+ _refresh();
49
+ const opt = isFunction(options) ? options() : options;
50
+ if (sub) sub.unsubscribe();
51
+ // reset state for a new request
52
+ isLoading.set(true);
53
+ error.set(undefined);
54
+ hasValue.set(false);
55
+ isEmpty.set(undefined);
56
+ // 优化:使用 Record<string, any> 替代 any,以提高类型安全性
57
+ const func = (EntityType as unknown as Record<string, any>)[method];
58
+ sub =
59
+ func &&
60
+ func(opt).subscribe({
61
+ next: (d: any) => {
62
+ isLoading.set(false);
63
+ hasValue.set(true);
64
+ if (Array.isArray(d)) {
65
+ isEmpty.set(d.length === 0);
66
+ } else if (d == null) {
67
+ // null/undefined treated as empty
68
+ isEmpty.set(true);
69
+ } else {
70
+ isEmpty.set(false);
71
+ }
72
+ _observer.next(d);
73
+ },
74
+ error: (e: any) => {
75
+ isLoading.set(false);
76
+ error.set(e);
77
+ },
78
+ complete: () => {
79
+ // ensure loading is false even if observable completes without next
80
+ isLoading.set(false);
81
+ }
82
+ });
83
+ });
84
+ const ob = new Observable<RT>(observer => {
85
+ _has_sub.set(true);
86
+ _observer = observer;
87
+ _observer.next(defaultValue);
88
+ return () => {
89
+ _eff.destroy();
90
+ sub?.unsubscribe();
91
+ };
92
+ });
93
+
94
+ const value = toLazySignal<RT>(ob, {
95
+ initialValue: defaultValue
96
+ }) as Signal<RT>;
97
+ const error = signal<Error | undefined>(undefined);
98
+ const isLoading = signal<boolean>(false);
99
+ const hasValue = signal<boolean>(false);
100
+ const isEmpty = signal<boolean | undefined>(undefined);
101
+ return {
102
+ value,
103
+ error,
104
+ isLoading,
105
+ isEmpty,
106
+ hasValue
107
+ };
108
+ };
109
+
110
+ /*
111
+ * Repository
112
+ */
113
+
114
+ /**
115
+ * 通过 ID 获取单个实体
116
+ *
117
+ * @param EntityType 实体类
118
+ * @param options 实体 ID 或查询参数对象
119
+ * @returns 返回包含实体的资源 signal
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const user = useGet(User, 'user-1');
124
+ * ```
125
+ */
126
+ export const useGet = <T extends EntityType>(EntityType: T, options: UseOptions<EntityStaticType<T, 'getOptions'>>) =>
127
+ useRepositoryQuery<T, InstanceType<T>>(EntityType, 'get', undefined, options);
128
+
129
+ /**
130
+ * 查找第一个匹配条件的实体
131
+ *
132
+ * @param EntityType 实体类
133
+ * @param options 查询参数(where、排序等)
134
+ * @returns 返回包含实体的资源 signal
135
+ */
136
+ export const useFindOne = <T extends EntityType>(
137
+ EntityType: T,
138
+ options: UseOptions<EntityStaticType<T, 'findOneOptions'>>
139
+ ) => useRepositoryQuery<T, InstanceType<T> | undefined>(EntityType, 'findOne', undefined, options);
140
+
141
+ /**
142
+ * 查找匹配的实体,未找到则抛出错误
143
+ *
144
+ * @param EntityType 实体类
145
+ * @param options 查询参数
146
+ * @returns 返回包含实体的资源 signal
147
+ */
148
+ export const useFindOneOrFail = <T extends EntityType>(
149
+ EntityType: T,
150
+ options: UseOptions<EntityStaticType<T, 'findOneOrFailOptions'>>
151
+ ) => useRepositoryQuery<T, InstanceType<T>>(EntityType, 'findOneOrFail', undefined, options);
152
+
153
+ /**
154
+ * 查找符合条件的多个实体
155
+ *
156
+ * @param EntityType 实体类
157
+ * @param options 查询参数
158
+ * @returns 返回包含实体数组的资源 signal
159
+ */
160
+ export const useFind = <T extends EntityType>(EntityType: T, options: UseOptions<EntityStaticType<T, 'findOptions'>>) =>
161
+ useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'find', [], options);
162
+
163
+ /**
164
+ * 使用游标分页查找实体
165
+ *
166
+ * @param EntityType 实体类
167
+ * @param options 游标参数
168
+ * @returns 返回包含实体数组的资源 signal
169
+ */
170
+ export const useFindByCursor = <T extends EntityType>(
171
+ EntityType: T,
172
+ options: UseOptions<EntityStaticType<T, 'findByCursorOptions'>>
173
+ ) => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'findByCursor', [], options);
174
+
175
+ /**
176
+ * 查找全部实体
177
+ *
178
+ * @param EntityType 实体类
179
+ * @param options 查询参数
180
+ * @returns 返回包含全部实体的资源 signal
181
+ */
182
+ export const useFindAll = <T extends EntityType>(
183
+ EntityType: T,
184
+ options: UseOptions<EntityStaticType<T, 'findAllOptions'>>
185
+ ) => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'findAll', [], options);
186
+
187
+ /**
188
+ * 统计满足条件的实体数量
189
+ *
190
+ * @param EntityType 实体类
191
+ * @param options 查询参数
192
+ * @returns 返回包含数量的资源 signal
193
+ */
194
+ export const useCount = <T extends EntityType>(
195
+ EntityType: T,
196
+ options: UseOptions<EntityStaticType<T, 'countOptions'>>
197
+ ) => useRepositoryQuery<T, number>(EntityType, 'count', undefined, options);
198
+
199
+ /*
200
+ * TreeRepository
201
+ */
202
+
203
+ /**
204
+ * 查找树结构中的所有子孙实体
205
+ *
206
+ * @param EntityType 实体类
207
+ * @param options 树查询参数(entityId、深度等)
208
+ * @returns 返回包含子孙实体的资源 signal
209
+ */
210
+ export const useFindDescendants = <T extends TreeEntityType>(
211
+ EntityType: T,
212
+ options: UseOptions<EntityStaticType<T, 'findTreeOptions'>>
213
+ ) => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'findDescendants', [], options);
214
+
215
+ /**
216
+ * 统计树结构中的子孙数量
217
+ *
218
+ * @param EntityType 实体类
219
+ * @param options 树查询参数
220
+ * @returns 返回包含数量的资源 signal
221
+ */
222
+ export const useCountDescendants = <T extends TreeEntityType>(
223
+ EntityType: T,
224
+ options: UseOptions<EntityStaticType<T, 'findTreeOptions'>>
225
+ ) => useRepositoryQuery<T, number>(EntityType, 'countDescendants', undefined, options);
226
+
227
+ /**
228
+ * 查找树结构中的所有祖先实体
229
+ *
230
+ * @param EntityType 实体类
231
+ * @param options 树查询参数
232
+ * @returns 返回包含祖先实体的资源 signal
233
+ */
234
+ export const useFindAncestors = <T extends TreeEntityType>(
235
+ EntityType: T,
236
+ options: UseOptions<EntityStaticType<T, 'findTreeOptions'>>
237
+ ) => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'findAncestors', [], options);
238
+
239
+ /**
240
+ * 统计树结构中的祖先数量
241
+ *
242
+ * @param EntityType 实体类
243
+ * @param options 树查询参数
244
+ * @returns 返回包含数量的资源 signal
245
+ */
246
+ export const useCountAncestors = <T extends TreeEntityType>(
247
+ EntityType: T,
248
+ options: UseOptions<EntityStaticType<T, 'findTreeOptions'>>
249
+ ) => useRepositoryQuery<T, number>(EntityType, 'countAncestors', undefined, options);
250
+
251
+ /*
252
+ * GraphRepository
253
+ */
254
+
255
+ /**
256
+ * 查找图结构中的邻接实体
257
+ *
258
+ * @param EntityType 实体类
259
+ * @param options 图查询参数(entityId、方向、层级等)
260
+ * @returns 返回包含邻居实体的资源 signal
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const friends = useGraphNeighbors(User, {
265
+ * entityId: 'user-1',
266
+ * direction: 'out',
267
+ * level: 1
268
+ * });
269
+ * ```
270
+ */
271
+ export const useGraphNeighbors = <T extends GraphEntityType>(
272
+ EntityType: T,
273
+ options: UseOptions<EntityStaticType<T, 'findNeighborsOptions'>>
274
+ ) => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'findNeighbors', [], options);
275
+
276
+ /**
277
+ * 统计图结构中的邻接数量
278
+ *
279
+ * @param EntityType 实体类
280
+ * @param options 图查询参数
281
+ * @returns 返回包含数量的资源 signal
282
+ */
283
+ export const useCountNeighbors = <T extends GraphEntityType>(
284
+ EntityType: T,
285
+ options: UseOptions<EntityStaticType<T, 'findNeighborsOptions'>>
286
+ ) => useRepositoryQuery<T, number>(EntityType, 'countNeighbors', 0, options);
287
+
288
+ /**
289
+ * 查找图结构中两个实体之间的路径
290
+ *
291
+ * @param EntityType 实体类
292
+ * @param options 路径查询参数(fromId、toId、最大深度等)
293
+ * @returns 返回包含路径的资源 signal
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const paths = useGraphPaths(User, {
298
+ * fromId: 'user-1',
299
+ * toId: 'user-2',
300
+ * maxDepth: 5
301
+ * });
302
+ * ```
303
+ */
304
+ export const useGraphPaths = <T extends GraphEntityType>(
305
+ EntityType: T,
306
+ options: UseOptions<EntityStaticType<T, 'findPathsOptions'>>
307
+ ) => useRepositoryQuery<T, any[]>(EntityType, 'findPaths', [], options);
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './hooks';
2
+ export * from './InfiniteScrollingList';
3
+ export { RxDBEntityChangeDirective } from './rxdb-change-detector.directive';
4
+ export { provideRxDB } from './rxdb.provider';
5
+ export * from './rxdb.service';
6
+ export * from './use-action';
7
+ export * from './use-state';
@@ -0,0 +1,94 @@
1
+ import { ChangeDetectionStrategy, Component, provideZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { Subject } from 'rxjs';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { RxDBEntityChangeDirective } from './rxdb-change-detector.directive';
6
+
7
+ // Mock entity type
8
+ const mockEntity = {
9
+ id: 'test-1',
10
+ name: 'Test Entity'
11
+ };
12
+
13
+ const mockPatchesSubject = new Subject();
14
+
15
+ // Mock getEntityStatus
16
+ vi.mock('@aiao/rxdb', () => ({
17
+ getEntityStatus: vi.fn(() => ({
18
+ patches$: mockPatchesSubject.asObservable()
19
+ }))
20
+ }));
21
+
22
+ @Component({
23
+ // eslint-disable-next-line @angular-eslint/component-selector
24
+ selector: 'test-component',
25
+ standalone: true,
26
+ imports: [RxDBEntityChangeDirective],
27
+ template: '<div [rxdbChangeDetector]="entity"></div>',
28
+ changeDetection: ChangeDetectionStrategy.OnPush
29
+ })
30
+ class TestComponent {
31
+ entity = mockEntity;
32
+ }
33
+
34
+ describe('RxDBEntityChangeDirective', () => {
35
+ beforeEach(() => {
36
+ TestBed.configureTestingModule({
37
+ providers: [provideZonelessChangeDetection()]
38
+ });
39
+ });
40
+
41
+ it('should create directive', () => {
42
+ const fixture = TestBed.createComponent(TestComponent);
43
+ expect(fixture).toBeDefined();
44
+ });
45
+
46
+ it('should subscribe to patches$ on initialization', () => {
47
+ const fixture = TestBed.createComponent(TestComponent);
48
+ fixture.detectChanges();
49
+
50
+ // Verify component was created successfully with the directive
51
+ expect(fixture.componentInstance).toBeDefined();
52
+
53
+ // 指令应该已经设置了订阅
54
+ // Testing the actual subscription behavior requires more complex mocking
55
+ });
56
+
57
+ it('should handle debounceTime input', () => {
58
+ @Component({
59
+ // eslint-disable-next-line @angular-eslint/component-selector
60
+ selector: 'test-debounce',
61
+ standalone: true,
62
+ imports: [RxDBEntityChangeDirective],
63
+ template: '<div [rxdbChangeDetector]="entity" [debounceTime]="100"></div>',
64
+ changeDetection: ChangeDetectionStrategy.OnPush
65
+ })
66
+ class TestDebounceComponent {
67
+ entity = mockEntity;
68
+ }
69
+
70
+ const fixture = TestBed.createComponent(TestDebounceComponent);
71
+ fixture.detectChanges();
72
+
73
+ expect(fixture.componentInstance).toBeDefined();
74
+ });
75
+
76
+ it('should handle auditTime input', () => {
77
+ @Component({
78
+ // eslint-disable-next-line @angular-eslint/component-selector
79
+ selector: 'test-audit',
80
+ standalone: true,
81
+ imports: [RxDBEntityChangeDirective],
82
+ template: '<div [rxdbChangeDetector]="entity" [auditTime]="50"></div>',
83
+ changeDetection: ChangeDetectionStrategy.OnPush
84
+ })
85
+ class TestAuditComponent {
86
+ entity = mockEntity;
87
+ }
88
+
89
+ const fixture = TestBed.createComponent(TestAuditComponent);
90
+ fixture.detectChanges();
91
+
92
+ expect(fixture.componentInstance).toBeDefined();
93
+ });
94
+ });
@@ -0,0 +1,35 @@
1
+ import { EntityType, getEntityStatus } from '@aiao/rxdb';
2
+ import { ChangeDetectorRef, DestroyRef, Directive, effect, inject, input } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { auditTime, debounceTime, Subscription } from 'rxjs';
5
+
6
+ /**
7
+ * RxDB 实体变化指令
8
+ * 如果你使用了 `changeDetection: ChangeDetectionStrategy.OnPush`
9
+ * 那么正在输入的变化不会触发 angular 渲染机制,使用这个指令可以实现实时渲染
10
+ *
11
+ * @example
12
+ * 当修改实体时,需要在多个地方预览修改变化时,可以使用这个指令
13
+ */
14
+ @Directive({ selector: '[rxdbChangeDetector]' })
15
+ export class RxDBEntityChangeDirective {
16
+ #destroyRef = inject(DestroyRef);
17
+ #changeDetectorRef = inject(ChangeDetectorRef);
18
+ #sub?: Subscription;
19
+ public readonly rxdbChangeDetector = input<InstanceType<EntityType>>();
20
+ public readonly debounceTime = input<number>(0);
21
+ public readonly auditTime = input<number>(0);
22
+
23
+ constructor() {
24
+ effect(() => {
25
+ const rxdbChange = this.rxdbChangeDetector();
26
+ const dt = this.debounceTime();
27
+ const at = this.auditTime();
28
+ const status = getEntityStatus(rxdbChange);
29
+ this.#sub?.unsubscribe();
30
+ this.#sub = status.patches$
31
+ .pipe(takeUntilDestroyed(this.#destroyRef), debounceTime(dt), auditTime(at))
32
+ .subscribe(() => this.#changeDetectorRef.markForCheck());
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ import { RxDB } from '@aiao/rxdb';
2
+ import { makeEnvironmentProviders } from '@angular/core';
3
+
4
+ /**
5
+ * RxDB 配置
6
+ */
7
+ export const provideRxDB = (useFactory?: () => RxDB) =>
8
+ makeEnvironmentProviders([
9
+ {
10
+ provide: RxDB,
11
+ useFactory
12
+ }
13
+ ]);
@@ -0,0 +1,31 @@
1
+ import { provideZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+ import { RxDBService } from './rxdb.service';
5
+
6
+ describe('RxDBService', () => {
7
+ let service: RxDBService;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ providers: [provideZonelessChangeDetection(), RxDBService]
12
+ });
13
+ service = TestBed.inject(RxDBService);
14
+ });
15
+
16
+ it('should be created', () => {
17
+ expect(service).toBeDefined();
18
+ });
19
+
20
+ it('should not throw when closing devtools if not open', () => {
21
+ expect(() => service.closeDevTools()).not.toThrow();
22
+ });
23
+
24
+ it('should not throw when setting theme if devtools not open', () => {
25
+ expect(() => service.setDevToolsTheme('dark')).not.toThrow();
26
+ });
27
+
28
+ // Note: Tests for devtools loading are skipped because they require
29
+ // full RxDB setup with database connections, which is better tested
30
+ // in integration tests with actual Angular applications.
31
+ });
@@ -0,0 +1,35 @@
1
+ import { ComponentRef, Injectable, ViewContainerRef } from '@angular/core';
2
+ import { RxDBDevtools } from './devtools/devtools';
3
+ import { RxDBDevToolsOptions } from './devtools/devtools.interface';
4
+
5
+ @Injectable({
6
+ providedIn: 'root'
7
+ })
8
+ export class RxDBService {
9
+ #componentRef?: ComponentRef<RxDBDevtools>;
10
+
11
+ async devtools(view: ViewContainerRef, options?: RxDBDevToolsOptions) {
12
+ if (this.#componentRef) {
13
+ this.closeDevTools();
14
+ return;
15
+ }
16
+ const { RxDBDevtools } = await import('./devtools/devtools.js');
17
+ const componentRef = view.createComponent(RxDBDevtools);
18
+ this.#componentRef = componentRef;
19
+ componentRef.setInput('componentRef', componentRef);
20
+ if (options) {
21
+ if (options.theme) this.setDevToolsTheme(options.theme);
22
+ }
23
+ componentRef.onDestroy(() => {
24
+ this.#componentRef = undefined;
25
+ });
26
+ }
27
+
28
+ closeDevTools() {
29
+ this.#componentRef?.instance.close_rxdb_devtools();
30
+ }
31
+
32
+ setDevToolsTheme(theme: 'light' | 'dark') {
33
+ this.#componentRef?.setInput('theme', theme);
34
+ }
35
+ }
@@ -0,0 +1,14 @@
1
+ import '@angular/compiler';
2
+
3
+ import { getTestBed } from '@angular/core/testing';
4
+ import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
5
+
6
+ const testBed = getTestBed();
7
+ if (!testBed.platform) {
8
+ testBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
9
+ }
10
+
11
+ import { afterEach } from 'vitest';
12
+ afterEach(() => {
13
+ getTestBed().resetTestingModule();
14
+ });
@@ -0,0 +1,88 @@
1
+ import { provideZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { useAction } from './use-action';
5
+
6
+ describe('useAction', () => {
7
+ beforeEach(() => {
8
+ TestBed.configureTestingModule({
9
+ providers: [provideZonelessChangeDetection()]
10
+ });
11
+ });
12
+ it('should create an action with isPending signal', () => {
13
+ TestBed.runInInjectionContext(() => {
14
+ const mockFn = vi.fn(async () => 'result');
15
+ const action = useAction(mockFn);
16
+
17
+ expect(action.isPending).toBeDefined();
18
+ expect(action.execute).toBeDefined();
19
+ expect(action.isPending()).toBe(false);
20
+ });
21
+ });
22
+
23
+ it('should set isPending to true during execution', async () => {
24
+ await TestBed.runInInjectionContext(async () => {
25
+ let resolvePromise: (value: string) => void;
26
+ const mockFn = vi.fn(
27
+ () =>
28
+ new Promise<string>(resolve => {
29
+ resolvePromise = resolve;
30
+ })
31
+ );
32
+ const action = useAction(mockFn);
33
+
34
+ const executePromise = action.execute();
35
+ expect(action.isPending()).toBe(true);
36
+
37
+ resolvePromise!('result');
38
+ await executePromise;
39
+ expect(action.isPending()).toBe(false);
40
+ });
41
+ });
42
+
43
+ it('should execute the promise function with options', async () => {
44
+ await TestBed.runInInjectionContext(async () => {
45
+ const mockFn = vi.fn(async (options?: { id: number }) => `result-${options?.id}`);
46
+ const action = useAction(mockFn);
47
+
48
+ const result = await action.execute({ id: 123 });
49
+
50
+ expect(mockFn).toHaveBeenCalledWith({ id: 123 });
51
+ expect(result).toBe('result-123');
52
+ });
53
+ });
54
+
55
+ it('should reset isPending to false after success', async () => {
56
+ await TestBed.runInInjectionContext(async () => {
57
+ const mockFn = vi.fn(async () => 'success');
58
+ const action = useAction(mockFn);
59
+
60
+ await action.execute();
61
+
62
+ expect(action.isPending()).toBe(false);
63
+ });
64
+ });
65
+
66
+ it('should reset isPending to false after error', async () => {
67
+ await TestBed.runInInjectionContext(async () => {
68
+ const mockFn = vi.fn(async () => {
69
+ throw new Error('test error');
70
+ });
71
+ const action = useAction(mockFn);
72
+
73
+ await expect(action.execute()).rejects.toThrow('test error');
74
+ expect(action.isPending()).toBe(false);
75
+ });
76
+ });
77
+
78
+ it('should return the result from promise function', async () => {
79
+ await TestBed.runInInjectionContext(async () => {
80
+ const mockFn = vi.fn(async () => ({ data: 'test' }));
81
+ const action = useAction(mockFn);
82
+
83
+ const result = await action.execute();
84
+
85
+ expect(result).toEqual({ data: 'test' });
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,20 @@
1
+ import { signal } from '@angular/core';
2
+
3
+ const action = <Options = any, RT = any>(promiseFn: (options?: Options) => Promise<RT>) => {
4
+ const isPending = signal<boolean>(false);
5
+ return {
6
+ isPending,
7
+ execute: async (options?: Options) => {
8
+ isPending.set(true);
9
+ try {
10
+ const d = await promiseFn(options);
11
+ return d;
12
+ } finally {
13
+ isPending.set(false);
14
+ }
15
+ }
16
+ };
17
+ };
18
+
19
+ export const useAction = <Options = any, RT = any>(promiseFn: (options?: Options) => Promise<RT>) =>
20
+ action<Options, RT>(promiseFn);