@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.
- package/eslint.config.mjs +58 -0
- package/ng-package.json +7 -0
- package/package.json +13 -23
- package/project.json +48 -0
- package/src/InfiniteScrollingList.ts +123 -0
- package/src/devtools/devtools.html +99 -0
- package/src/devtools/devtools.interface.ts +3 -0
- package/src/devtools/devtools.scss +49 -0
- package/src/devtools/devtools.spec.ts +30 -0
- package/src/devtools/devtools.ts +207 -0
- package/src/devtools/entity-table-item.ts +47 -0
- package/src/devtools/entity-table-tools.html +56 -0
- package/src/devtools/entity-table-tools.scss +8 -0
- package/src/devtools/entity-table-tools.ts +153 -0
- package/src/devtools/event-tools.html +15 -0
- package/src/devtools/event-tools.scss +18 -0
- package/src/devtools/event-tools.ts +45 -0
- package/src/devtools/scroll-advanced-calc.service.ts +41 -0
- package/src/devtools/settings.html +46 -0
- package/src/devtools/settings.scss +19 -0
- package/src/devtools/settings.ts +122 -0
- package/src/hooks.ts +307 -0
- package/src/index.ts +7 -0
- package/src/rxdb-change-detector.directive.spec.ts +94 -0
- package/src/rxdb-change-detector.directive.ts +35 -0
- package/src/rxdb.provider.ts +13 -0
- package/src/rxdb.service.spec.ts +31 -0
- package/src/rxdb.service.ts +35 -0
- package/src/test-setup.ts +14 -0
- package/src/use-action.spec.ts +88 -0
- package/src/use-action.ts +20 -0
- package/src/use-state.spec.ts +105 -0
- package/src/use-state.ts +28 -0
- package/tsconfig.json +33 -0
- package/tsconfig.lib.json +42 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +23 -0
- package/vite.config.mts +55 -0
- package/fesm2022/aiao-rxdb-angular.mjs +0 -47
- package/fesm2022/aiao-rxdb-angular.mjs.map +0 -1
- 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,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);
|