@ethlete/query 5.15.0 → 5.16.0

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 (184) hide show
  1. package/.eslintrc.json +40 -0
  2. package/CHANGELOG.md +14 -0
  3. package/jest.config.ts +23 -0
  4. package/ng-package.json +8 -0
  5. package/package.json +6 -23
  6. package/project.json +37 -0
  7. package/{index.d.ts → src/index.ts} +2 -0
  8. package/src/lib/auth/auth-provider.types.ts +160 -0
  9. package/src/lib/auth/auth-provider.utils.ts +39 -0
  10. package/src/lib/auth/basic-auth-provider.ts +17 -0
  11. package/src/lib/auth/bearer-auth-provider.ts +243 -0
  12. package/src/lib/auth/custom-header-auth-provider.ts +13 -0
  13. package/{lib/class-validator/class-validator.types.d.ts → src/lib/class-validator/class-validator.types.ts} +4 -3
  14. package/src/lib/class-validator/class-validator.utils.ts +5 -0
  15. package/src/lib/devtools/components/query-devtools/query-devtools.component.html +402 -0
  16. package/src/lib/devtools/components/query-devtools/query-devtools.component.scss +360 -0
  17. package/src/lib/devtools/components/query-devtools/query-devtools.component.ts +427 -0
  18. package/src/lib/devtools/pipes/query-short-name.pipe.ts +24 -0
  19. package/src/lib/devtools/utils/query-client.utils.ts +25 -0
  20. package/src/lib/directives/infinity-query-trigger.directive.ts +58 -0
  21. package/src/lib/directives/infinity-query.directive.ts +208 -0
  22. package/src/lib/directives/query.directive.ts +201 -0
  23. package/src/lib/entity/entity-store.ts +180 -0
  24. package/src/lib/entity/entity.types.ts +34 -0
  25. package/src/lib/entity/entity.utils.ts +51 -0
  26. package/src/lib/gql/gql.ts +22 -0
  27. package/src/lib/infinite-query/infinity-query.ts +245 -0
  28. package/src/lib/infinite-query/infinity-query.types.ts +149 -0
  29. package/src/lib/infinite-query/infinity-query.utils.ts +14 -0
  30. package/src/lib/logger/logger.ts +30 -0
  31. package/src/lib/query/query.ts +631 -0
  32. package/src/lib/query/query.types.ts +532 -0
  33. package/src/lib/query/query.utils.spec.ts +92 -0
  34. package/src/lib/query/query.utils.ts +450 -0
  35. package/src/lib/query-client/query-client.spec.ts +77 -0
  36. package/src/lib/query-client/query-client.ts +195 -0
  37. package/src/lib/query-client/query-client.types.ts +101 -0
  38. package/src/lib/query-client/query-client.utils.spec.ts +61 -0
  39. package/src/lib/query-client/query-client.utils.ts +28 -0
  40. package/src/lib/query-creator/query-creator.ts +103 -0
  41. package/src/lib/query-creator/query-creator.types.ts +74 -0
  42. package/src/lib/query-form/query-form.ts +526 -0
  43. package/src/lib/query-form/query-form.types.ts +169 -0
  44. package/src/lib/query-form/query-form.utils.ts +116 -0
  45. package/src/lib/query-store/query-store.ts +192 -0
  46. package/src/lib/request/request.ts +253 -0
  47. package/src/lib/request/request.types.ts +164 -0
  48. package/src/lib/request/request.util.spec.ts +177 -0
  49. package/src/lib/request/request.util.ts +399 -0
  50. package/src/lib/stories/angular-example-page.mdx +85 -0
  51. package/src/lib/stories/authentication-page.mdx +35 -0
  52. package/src/lib/stories/changelog-page.mdx +9 -0
  53. package/src/lib/stories/client-page.mdx +100 -0
  54. package/src/lib/stories/queries-page.mdx +117 -0
  55. package/src/lib/symfony/symfony.types.ts +21 -0
  56. package/src/lib/symfony/symfony.utils.ts +43 -0
  57. package/src/lib/utils/data.utils.ts +280 -0
  58. package/src/lib/utils/deep-freeze.util.ts +21 -0
  59. package/{lib/utils/def.util.d.ts → src/lib/utils/def.util.ts} +1 -1
  60. package/src/test-setup.ts +1 -0
  61. package/tsconfig.json +29 -0
  62. package/tsconfig.lib.json +12 -0
  63. package/tsconfig.lib.prod.json +10 -0
  64. package/tsconfig.spec.json +11 -0
  65. package/esm2022/ethlete-query.mjs +0 -5
  66. package/esm2022/index.mjs +0 -20
  67. package/esm2022/lib/auth/auth-provider.types.mjs +0 -2
  68. package/esm2022/lib/auth/auth-provider.utils.mjs +0 -25
  69. package/esm2022/lib/auth/basic-auth-provider.mjs +0 -15
  70. package/esm2022/lib/auth/bearer-auth-provider.mjs +0 -168
  71. package/esm2022/lib/auth/custom-header-auth-provider.mjs +0 -12
  72. package/esm2022/lib/auth/index.mjs +0 -6
  73. package/esm2022/lib/class-validator/class-validator.types.mjs +0 -2
  74. package/esm2022/lib/class-validator/class-validator.utils.mjs +0 -4
  75. package/esm2022/lib/class-validator/index.mjs +0 -3
  76. package/esm2022/lib/devtools/components/query-devtools/index.mjs +0 -2
  77. package/esm2022/lib/devtools/components/query-devtools/query-devtools.component.mjs +0 -297
  78. package/esm2022/lib/devtools/pipes/index.mjs +0 -2
  79. package/esm2022/lib/devtools/pipes/query-short-name.pipe.mjs +0 -24
  80. package/esm2022/lib/devtools/utils/index.mjs +0 -2
  81. package/esm2022/lib/devtools/utils/query-client.utils.mjs +0 -13
  82. package/esm2022/lib/directives/index.mjs +0 -4
  83. package/esm2022/lib/directives/infinity-query-trigger.directive.mjs +0 -56
  84. package/esm2022/lib/directives/infinity-query.directive.mjs +0 -147
  85. package/esm2022/lib/directives/query.directive.mjs +0 -123
  86. package/esm2022/lib/entity/entity-store.mjs +0 -127
  87. package/esm2022/lib/entity/entity.types.mjs +0 -2
  88. package/esm2022/lib/entity/entity.utils.mjs +0 -29
  89. package/esm2022/lib/entity/index.mjs +0 -4
  90. package/esm2022/lib/gql/gql.mjs +0 -23
  91. package/esm2022/lib/gql/index.mjs +0 -2
  92. package/esm2022/lib/infinite-query/index.mjs +0 -4
  93. package/esm2022/lib/infinite-query/infinity-query.mjs +0 -171
  94. package/esm2022/lib/infinite-query/infinity-query.types.mjs +0 -2
  95. package/esm2022/lib/infinite-query/infinity-query.utils.mjs +0 -3
  96. package/esm2022/lib/logger/index.mjs +0 -2
  97. package/esm2022/lib/logger/logger.mjs +0 -16
  98. package/esm2022/lib/query/index.mjs +0 -4
  99. package/esm2022/lib/query/query.mjs +0 -428
  100. package/esm2022/lib/query/query.types.mjs +0 -2
  101. package/esm2022/lib/query/query.utils.mjs +0 -253
  102. package/esm2022/lib/query-client/index.mjs +0 -4
  103. package/esm2022/lib/query-client/query-client.mjs +0 -80
  104. package/esm2022/lib/query-client/query-client.types.mjs +0 -2
  105. package/esm2022/lib/query-client/query-client.utils.mjs +0 -20
  106. package/esm2022/lib/query-creator/index.mjs +0 -3
  107. package/esm2022/lib/query-creator/query-creator.mjs +0 -55
  108. package/esm2022/lib/query-creator/query-creator.types.mjs +0 -2
  109. package/esm2022/lib/query-form/index.mjs +0 -4
  110. package/esm2022/lib/query-form/query-form.mjs +0 -368
  111. package/esm2022/lib/query-form/query-form.types.mjs +0 -2
  112. package/esm2022/lib/query-form/query-form.utils.mjs +0 -97
  113. package/esm2022/lib/query-store/index.mjs +0 -2
  114. package/esm2022/lib/query-store/query-store.mjs +0 -144
  115. package/esm2022/lib/request/index.mjs +0 -4
  116. package/esm2022/lib/request/request.mjs +0 -196
  117. package/esm2022/lib/request/request.types.mjs +0 -2
  118. package/esm2022/lib/request/request.util.mjs +0 -252
  119. package/esm2022/lib/symfony/index.mjs +0 -3
  120. package/esm2022/lib/symfony/symfony.types.mjs +0 -2
  121. package/esm2022/lib/symfony/symfony.utils.mjs +0 -31
  122. package/esm2022/lib/utils/data.utils.mjs +0 -138
  123. package/esm2022/lib/utils/deep-freeze.util.mjs +0 -17
  124. package/esm2022/lib/utils/def.util.mjs +0 -8
  125. package/esm2022/lib/utils/index.mjs +0 -4
  126. package/fesm2022/ethlete-query.mjs +0 -3276
  127. package/fesm2022/ethlete-query.mjs.map +0 -1
  128. package/lib/auth/auth-provider.types.d.ts +0 -133
  129. package/lib/auth/auth-provider.utils.d.ts +0 -9
  130. package/lib/auth/basic-auth-provider.d.ts +0 -10
  131. package/lib/auth/bearer-auth-provider.d.ts +0 -23
  132. package/lib/auth/custom-header-auth-provider.d.ts +0 -9
  133. package/lib/class-validator/class-validator.utils.d.ts +0 -2
  134. package/lib/devtools/components/query-devtools/query-devtools.component.d.ts +0 -151
  135. package/lib/devtools/pipes/query-short-name.pipe.d.ts +0 -8
  136. package/lib/devtools/utils/query-client.utils.d.ts +0 -12
  137. package/lib/directives/infinity-query-trigger.directive.d.ts +0 -15
  138. package/lib/directives/infinity-query.directive.d.ts +0 -49
  139. package/lib/directives/query.directive.d.ts +0 -63
  140. package/lib/entity/entity-store.d.ts +0 -25
  141. package/lib/entity/entity.types.d.ts +0 -25
  142. package/lib/entity/entity.utils.d.ts +0 -12
  143. package/lib/gql/gql.d.ts +0 -6
  144. package/lib/infinite-query/infinity-query.d.ts +0 -29
  145. package/lib/infinite-query/infinity-query.types.d.ts +0 -104
  146. package/lib/infinite-query/infinity-query.utils.d.ts +0 -4
  147. package/lib/logger/logger.d.ts +0 -12
  148. package/lib/query/query.d.ts +0 -60
  149. package/lib/query/query.types.d.ts +0 -388
  150. package/lib/query/query.utils.d.ts +0 -91
  151. package/lib/query-client/query-client.d.ts +0 -29
  152. package/lib/query-client/query-client.types.d.ts +0 -82
  153. package/lib/query-client/query-client.utils.d.ts +0 -4
  154. package/lib/query-creator/query-creator.d.ts +0 -20
  155. package/lib/query-creator/query-creator.types.d.ts +0 -13
  156. package/lib/query-form/query-form.d.ts +0 -89
  157. package/lib/query-form/query-form.types.d.ts +0 -138
  158. package/lib/query-form/query-form.utils.d.ts +0 -13
  159. package/lib/query-store/query-store.d.ts +0 -31
  160. package/lib/request/request.d.ts +0 -3
  161. package/lib/request/request.types.d.ts +0 -126
  162. package/lib/request/request.util.d.ts +0 -92
  163. package/lib/symfony/symfony.types.d.ts +0 -19
  164. package/lib/symfony/symfony.utils.d.ts +0 -6
  165. package/lib/utils/data.utils.d.ts +0 -59
  166. package/lib/utils/deep-freeze.util.d.ts +0 -1
  167. /package/{lib/auth/index.d.ts → src/lib/auth/index.ts} +0 -0
  168. /package/{lib/class-validator/index.d.ts → src/lib/class-validator/index.ts} +0 -0
  169. /package/{lib/devtools/components/query-devtools/index.d.ts → src/lib/devtools/components/query-devtools/index.ts} +0 -0
  170. /package/{lib/devtools/pipes/index.d.ts → src/lib/devtools/pipes/index.ts} +0 -0
  171. /package/{lib/devtools/utils/index.d.ts → src/lib/devtools/utils/index.ts} +0 -0
  172. /package/{lib/directives/index.d.ts → src/lib/directives/index.ts} +0 -0
  173. /package/{lib/entity/index.d.ts → src/lib/entity/index.ts} +0 -0
  174. /package/{lib/gql/index.d.ts → src/lib/gql/index.ts} +0 -0
  175. /package/{lib/infinite-query/index.d.ts → src/lib/infinite-query/index.ts} +0 -0
  176. /package/{lib/logger/index.d.ts → src/lib/logger/index.ts} +0 -0
  177. /package/{lib/query/index.d.ts → src/lib/query/index.ts} +0 -0
  178. /package/{lib/query-client/index.d.ts → src/lib/query-client/index.ts} +0 -0
  179. /package/{lib/query-creator/index.d.ts → src/lib/query-creator/index.ts} +0 -0
  180. /package/{lib/query-form/index.d.ts → src/lib/query-form/index.ts} +0 -0
  181. /package/{lib/query-store/index.d.ts → src/lib/query-store/index.ts} +0 -0
  182. /package/{lib/request/index.d.ts → src/lib/request/index.ts} +0 -0
  183. /package/{lib/symfony/index.d.ts → src/lib/symfony/index.ts} +0 -0
  184. /package/{lib/utils/index.d.ts → src/lib/utils/index.ts} +0 -0
@@ -0,0 +1,526 @@
1
+ import { NgZone, assertInInjectionContext, inject, isDevMode } from '@angular/core';
2
+ import { toSignal } from '@angular/core/rxjs-interop';
3
+ import { FormGroup } from '@angular/forms';
4
+ import { ActivatedRoute, Router } from '@angular/router';
5
+ import { ET_PROPERTY_REMOVED, RouterStateService, clone, createDestroy, equal } from '@ethlete/core';
6
+ import { BehaviorSubject, Subject, debounceTime, map, merge, of, switchMap, takeUntil, tap, timer } from 'rxjs';
7
+ import {
8
+ QueryFieldOptions,
9
+ QueryFormGroup,
10
+ QueryFormGroupControls,
11
+ QueryFormObserveOptions,
12
+ QueryFormValue,
13
+ QueryFormValueEvent,
14
+ QueryFormWriteOptions,
15
+ } from './query-form.types';
16
+ import { transformToBoolean, transformToNumber } from './query-form.utils';
17
+
18
+ const ET_ARR_PREFIX = 'ET_ARR__';
19
+
20
+ export class QueryField<T> {
21
+ get control() {
22
+ return this.data.control;
23
+ }
24
+
25
+ constructor(public data: QueryFieldOptions<T>) {}
26
+ }
27
+
28
+ const IGNORED_FILTER_COUNT_FIELDS = ['page', 'skip', 'take', 'limit', 'sort', 'sortBy', 'sortOrder', 'query', 'search'];
29
+
30
+ export interface QueryFormOptions {
31
+ /**
32
+ * A prefix to use for the query parameters. This is useful when you have multiple query forms on the same page.
33
+ */
34
+ queryParamPrefix?: string | (() => string);
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ export type AnyQueryForm = QueryForm<any>;
39
+
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ export class QueryForm<T extends Record<string, QueryField<any>>> {
42
+ private readonly _destroy$ = createDestroy();
43
+ private readonly _unobserveTrigger$ = new Subject<boolean>();
44
+ private readonly _router = inject(Router);
45
+ private readonly _activatedRoute = inject(ActivatedRoute);
46
+ private readonly _defaultValues = this._extractDefaultValues();
47
+ private readonly _routerStateService = inject(RouterStateService);
48
+ private readonly _zone = inject(NgZone);
49
+ private readonly _didValueChanges$ = new Subject<boolean>();
50
+
51
+ private _isObserving = false;
52
+ private _skipNextResets = false;
53
+
54
+ /**
55
+ * The angular form group that contains all the fields.
56
+ *
57
+ * **Do not** use any of the following methods on this form group:
58
+ * - `setValue`: Use `QueryForm.setValue` instead.
59
+ * - `patchValue`: Use `QueryForm.patchValue` instead.
60
+ * - `valueChanges`: Use `QueryForm.changes$` instead.
61
+ * - `value`: Use `QueryForm.value` instead.
62
+ * - `controls`: Use `QueryForm.controls` instead.
63
+ */
64
+ readonly form = this._setupFormGroup();
65
+
66
+ private readonly _lastFormValue$ = new BehaviorSubject<QueryFormValue<T> | null>(null);
67
+ private readonly _currentFormValue$ = new BehaviorSubject<QueryFormValue<T>>(this._formValue);
68
+
69
+ private get _formValue() {
70
+ return this.form.getRawValue() as QueryFormValue<T>;
71
+ }
72
+
73
+ private get _form() {
74
+ return this.form as unknown as QueryFormGroup;
75
+ }
76
+
77
+ private readonly _changes$ = new BehaviorSubject<QueryFormValueEvent<T>>({
78
+ previousValue: null,
79
+ currentValue: this.form.getRawValue() as QueryFormValue<T>,
80
+ });
81
+
82
+ readonly changes$ = this._changes$.asObservable();
83
+ readonly currentValue$ = this._changes$.pipe(map(({ currentValue }) => currentValue));
84
+ readonly previousValue$ = this._changes$.pipe(map(({ previousValue }) => previousValue));
85
+
86
+ readonly changes = toSignal(this.changes$);
87
+ readonly currentValue = toSignal(this.currentValue$);
88
+ readonly previousValue = toSignal(this.previousValue$);
89
+
90
+ /**
91
+ * The number of active filters.
92
+ *
93
+ * Excludes the following fields by default:
94
+ * - `page`
95
+ * - `skip`
96
+ * - `take`
97
+ * - `limit`
98
+ * - `sort`
99
+ * - `sortBy`
100
+ * - `sortOrder`
101
+ * - `query`
102
+ * - `search`
103
+ */
104
+ readonly activeFilterCount$ = this.changes$.pipe(
105
+ map(
106
+ ({ currentValue }) =>
107
+ Object.entries(currentValue)
108
+ .map(([key, value]) => {
109
+ if (IGNORED_FILTER_COUNT_FIELDS.includes(key) || this._fields[key]?.data.skipInFilterCount) {
110
+ return true;
111
+ }
112
+
113
+ return this._isDefaultValue(key, value);
114
+ })
115
+ .filter((v) => v === false).length,
116
+ ),
117
+ );
118
+
119
+ get controls() {
120
+ return this.form.controls;
121
+ }
122
+
123
+ get value() {
124
+ return this._changes$.value.currentValue;
125
+ }
126
+
127
+ get defaultFormValue() {
128
+ return Object.entries(this._defaultValues).reduce((acc, [key]) => {
129
+ acc[key as keyof QueryFormValue<T>] = this._getDefaultValue(key);
130
+
131
+ return acc;
132
+ }, {} as QueryFormValue<T>);
133
+ }
134
+
135
+ // with prefix eg. page should become ${prefix}-page
136
+ constructor(
137
+ private _fields: T,
138
+ private _options?: QueryFormOptions,
139
+ ) {
140
+ assertInInjectionContext(QueryForm);
141
+ }
142
+
143
+ observe(options?: QueryFormObserveOptions) {
144
+ if (this._isObserving) {
145
+ if (isDevMode()) {
146
+ console.warn('QueryForm.observe() was called multiple times. This is not supported.');
147
+ }
148
+ return this;
149
+ }
150
+
151
+ this._isObserving = true;
152
+
153
+ if (options?.syncOnNavigation !== false) {
154
+ const didChanges = this.setFormValueFromUrlQueryParams({
155
+ queryParams: this._routerStateService.queryParams,
156
+ });
157
+
158
+ if (didChanges) {
159
+ this._handleFormChange(true);
160
+ }
161
+ }
162
+
163
+ merge(...Object.values(this._fields).map((field) => field.control.valueChanges), this._didValueChanges$)
164
+ .pipe(
165
+ debounceTime(0),
166
+ tap(() => {
167
+ this._handleFormChange();
168
+ }),
169
+ takeUntil(this._destroy$),
170
+ takeUntil(this._unobserveTrigger$),
171
+ )
172
+ .subscribe();
173
+
174
+ let changedFieldsInLastResetLoop: string[] = [];
175
+ let currentUniqueChangedFields: string[] = [];
176
+
177
+ this._currentFormValue$
178
+ .pipe(
179
+ map((currentValue) => {
180
+ return {
181
+ previousValue: clone(this._lastFormValue$.value),
182
+ currentValue: clone(currentValue),
183
+ };
184
+ }),
185
+ tap(({ currentValue, previousValue }) => {
186
+ if (options?.writeToQueryParams !== false) {
187
+ this._syncViaUrlQueryParams(currentValue, options?.replaceUrl);
188
+ }
189
+
190
+ const didResetValues = this._skipNextResets
191
+ ? false
192
+ : this._handleQueryFormResets(previousValue ?? null, currentValue);
193
+
194
+ this._skipNextResets = false;
195
+
196
+ const changedFields = Object.keys(currentValue).filter(
197
+ (key) => !equal(previousValue?.[key], currentValue[key]),
198
+ );
199
+
200
+ if (changedFieldsInLastResetLoop.length) {
201
+ changedFields.push(...changedFieldsInLastResetLoop);
202
+ changedFieldsInLastResetLoop = [];
203
+ }
204
+
205
+ if (didResetValues) {
206
+ this._didValueChanges$.next(true);
207
+ changedFieldsInLastResetLoop = changedFields;
208
+ }
209
+
210
+ currentUniqueChangedFields = [...new Set(changedFields)];
211
+ }),
212
+ switchMap(({ currentValue, previousValue }) => {
213
+ if (changedFieldsInLastResetLoop.length) return of(null).pipe(map(() => ({ currentValue, previousValue })));
214
+
215
+ const debounceValues = currentUniqueChangedFields.map((key) => {
216
+ const field = this._fields[key];
217
+
218
+ if (!field) return null;
219
+
220
+ if (field.data.disableDebounceIfFalsy === true && !currentValue[key]) {
221
+ return null;
222
+ }
223
+
224
+ return field.data.debounce ?? null;
225
+ });
226
+
227
+ currentUniqueChangedFields = [];
228
+
229
+ if (debounceValues.some((v) => v === null) || !debounceValues.length) {
230
+ return of(null).pipe(map(() => ({ currentValue, previousValue })));
231
+ }
232
+
233
+ const shortestDebounceTime = Math.min(...debounceValues.filter((v): v is number => v !== null));
234
+
235
+ return timer(shortestDebounceTime).pipe(map(() => ({ currentValue, previousValue })));
236
+ }),
237
+ tap(({ currentValue, previousValue }) => {
238
+ if (changedFieldsInLastResetLoop.length) return;
239
+
240
+ this._changes$.next({
241
+ previousValue: previousValue ?? null,
242
+ currentValue: currentValue,
243
+ });
244
+ }),
245
+ takeUntil(this._destroy$),
246
+ takeUntil(this._unobserveTrigger$),
247
+ )
248
+ .subscribe();
249
+
250
+ if (options?.syncOnNavigation !== false) {
251
+ this._routerStateService.queryParamChanges$
252
+ .pipe(
253
+ takeUntil(this._destroy$),
254
+ takeUntil(this._unobserveTrigger$),
255
+ tap((changes) => {
256
+ const didValueChanges = this.setFormValueFromUrlQueryParams({ queryParams: changes });
257
+
258
+ if (didValueChanges) {
259
+ this._didValueChanges$.next(true);
260
+ }
261
+ }),
262
+ )
263
+ .subscribe();
264
+ }
265
+
266
+ return this;
267
+ }
268
+
269
+ private _handleQueryFormResets(previousValue: QueryFormValue<T> | null, currentValue: QueryFormValue<T>) {
270
+ let didResetValues = false;
271
+
272
+ for (const formFieldKey in this._fields) {
273
+ const field = this._fields[formFieldKey];
274
+
275
+ if (!field) continue;
276
+
277
+ const resets = field.data.isResetBy;
278
+
279
+ if (!resets) continue;
280
+
281
+ const resetConditionKeys = Array.isArray(resets) ? resets : [resets];
282
+
283
+ for (const resetConditionKey of resetConditionKeys) {
284
+ if (!(resetConditionKey in this._fields)) {
285
+ if (isDevMode()) {
286
+ console.warn(`The field "${resetConditionKey}" is not defined in the QueryForm. Is it a typo?`, this);
287
+ }
288
+
289
+ continue;
290
+ }
291
+
292
+ if (
293
+ previousValue &&
294
+ currentValue &&
295
+ !equal(previousValue[resetConditionKey], currentValue[resetConditionKey])
296
+ ) {
297
+ const defaultValueForKeyToReset = this._getDefaultValue(formFieldKey);
298
+ const currentValueForKeyToReset = currentValue[formFieldKey];
299
+
300
+ if (equal(defaultValueForKeyToReset, currentValueForKeyToReset)) {
301
+ continue;
302
+ }
303
+
304
+ this.form.controls[formFieldKey].setValue(defaultValueForKeyToReset);
305
+
306
+ didResetValues = true;
307
+
308
+ break;
309
+ }
310
+ }
311
+ }
312
+
313
+ return didResetValues;
314
+ }
315
+
316
+ unobserve() {
317
+ this._unobserveTrigger$.next(true);
318
+ }
319
+
320
+ setFormValueFromUrlQueryParams(options: { queryParams: Record<string, unknown> }) {
321
+ let didValueChanges = false;
322
+
323
+ for (const [key, field] of Object.entries(this._fields)) {
324
+ const value = options.queryParams[this._transformKeyToQueryParam(key)];
325
+
326
+ const valueDoesNotExist = value === undefined;
327
+
328
+ if (valueDoesNotExist) continue;
329
+
330
+ const valueGotRemoved = value === ET_PROPERTY_REMOVED;
331
+
332
+ if (valueGotRemoved) {
333
+ field.control.setValue(this._getDefaultValue(key), { emitEvent: false });
334
+ didValueChanges = true;
335
+
336
+ continue;
337
+ }
338
+ let deserializedValue = value;
339
+
340
+ if (field.data.queryParamToValueTransformFn) {
341
+ deserializedValue = field.data.queryParamToValueTransformFn(value);
342
+ } else if (typeof this._getDefaultValue(key) === 'number' || !isNaN(Number(value))) {
343
+ deserializedValue = transformToNumber(value);
344
+ } else if (typeof this._getDefaultValue(key) === 'boolean' || value === 'true' || value === 'false') {
345
+ deserializedValue = transformToBoolean(value);
346
+ }
347
+
348
+ const valueIsEqualToCurrent = equal(deserializedValue, field.control.value);
349
+
350
+ if (valueIsEqualToCurrent) continue;
351
+
352
+ field.control.setValue(deserializedValue, { emitEvent: false });
353
+ didValueChanges = true;
354
+ }
355
+
356
+ return didValueChanges;
357
+ }
358
+
359
+ setValue(value: QueryFormValue<T>, options?: QueryFormWriteOptions) {
360
+ this._form._setValue(value, options);
361
+
362
+ if (options?.skipResets) {
363
+ this._skipNextResets = true;
364
+ }
365
+ }
366
+
367
+ patchValue(value: Partial<QueryFormValue<T>>, options?: QueryFormWriteOptions) {
368
+ this._form._patchValue(value, options);
369
+
370
+ if (options?.skipResets) {
371
+ this._skipNextResets = true;
372
+ }
373
+ }
374
+
375
+ resetFieldToDefault(key: keyof QueryFormValue<T>, options?: QueryFormWriteOptions) {
376
+ const defaultValue = this._getDefaultValue(key as string);
377
+
378
+ this.form.controls[key].setValue(defaultValue);
379
+
380
+ if (options?.skipResets) {
381
+ this._skipNextResets = true;
382
+ }
383
+ }
384
+
385
+ resetFieldsToDefault(keys: (keyof QueryFormValue<T>)[], options?: QueryFormWriteOptions) {
386
+ const defaults = keys.reduce(
387
+ (acc, key) => {
388
+ acc[key] = this._getDefaultValue(key as string);
389
+
390
+ return acc;
391
+ },
392
+ {} as Partial<QueryFormValue<T>>,
393
+ );
394
+
395
+ this.patchValue(defaults);
396
+
397
+ if (options?.skipResets) {
398
+ this._skipNextResets = true;
399
+ }
400
+ }
401
+
402
+ resetAllFieldsToDefault(options?: QueryFormWriteOptions & { skipFields?: (keyof QueryFormValue<T>)[] }) {
403
+ const keys = Object.keys(this._fields) as (keyof QueryFormValue<T>)[];
404
+
405
+ if (options?.skipFields) {
406
+ for (const key of options.skipFields) {
407
+ const index = keys.indexOf(key);
408
+
409
+ if (index !== -1) {
410
+ keys.splice(index, 1);
411
+ }
412
+ }
413
+ }
414
+
415
+ this.resetFieldsToDefault(keys, options);
416
+ }
417
+
418
+ private _getDefaultValue(key: string) {
419
+ const val = this._defaultValues[key];
420
+
421
+ if (typeof val === 'string' && val.startsWith(ET_ARR_PREFIX)) {
422
+ return JSON.parse(val.slice(ET_ARR_PREFIX.length));
423
+ }
424
+
425
+ return val ?? null;
426
+ }
427
+
428
+ private _transformKeyToQueryParam(key: string) {
429
+ if (!this._options?.queryParamPrefix) return key;
430
+
431
+ const prefix =
432
+ typeof this._options?.queryParamPrefix === 'string'
433
+ ? this._options.queryParamPrefix
434
+ : this._options.queryParamPrefix();
435
+
436
+ return `${prefix}-${key}`;
437
+ }
438
+
439
+ private _isDefaultValue(key: string, value: unknown) {
440
+ const normalizedValue = Array.isArray(value) ? `${ET_ARR_PREFIX}${JSON.stringify(value)}` : value;
441
+
442
+ return this._getDefaultValue(key) === normalizedValue;
443
+ }
444
+
445
+ private _setupFormGroup() {
446
+ const group = new FormGroup({} as QueryFormGroupControls<T>) as unknown as QueryFormGroup;
447
+
448
+ for (const [key, field] of Object.entries(this._fields)) {
449
+ group.addControl(key, field.control);
450
+ }
451
+
452
+ group._patchValue = group.patchValue;
453
+ group._setValue = group.setValue;
454
+
455
+ group.patchValue = this.patchValue.bind(this);
456
+ group.setValue = this.setValue.bind(this);
457
+
458
+ return group as unknown as FormGroup<QueryFormGroupControls<T>>;
459
+ }
460
+
461
+ private _extractDefaultValues() {
462
+ const defaultValues: Record<string, unknown> = {};
463
+
464
+ for (const [key, field] of Object.entries(this._fields)) {
465
+ const value = field.data.defaultValue !== undefined ? field.data.defaultValue : field.control.value;
466
+
467
+ if (Array.isArray(value)) {
468
+ defaultValues[key] = `${ET_ARR_PREFIX}${JSON.stringify(value)}`;
469
+ } else {
470
+ defaultValues[key] = value;
471
+ }
472
+ }
473
+
474
+ return defaultValues;
475
+ }
476
+
477
+ private _syncViaUrlQueryParams(values: QueryFormValue<T>, replaceUrl?: boolean) {
478
+ const queryParams = { ...clone(this._activatedRoute.snapshot.queryParams) };
479
+
480
+ for (const [key, value] of Object.entries(values)) {
481
+ const queryParamKey = this._transformKeyToQueryParam(key);
482
+ const field = this._fields[key];
483
+
484
+ if (!field) {
485
+ continue;
486
+ }
487
+
488
+ const isDefault = this._isDefaultValue(key, value);
489
+ const writeDefaultToUrl = field.data.appendDefaultValueToUrl === true;
490
+ const writeToUrl = field.data.appendToUrl !== false;
491
+
492
+ if (!writeToUrl || (isDefault && !writeDefaultToUrl)) {
493
+ queryParams[queryParamKey] = undefined;
494
+ } else {
495
+ queryParams[queryParamKey] = field.data.valueToQueryParamTransformFn
496
+ ? field.data.valueToQueryParamTransformFn?.(value)
497
+ : value;
498
+ }
499
+ }
500
+
501
+ this._zone.run(() => {
502
+ this._router.navigate([], {
503
+ queryParams,
504
+ replaceUrl,
505
+ queryParamsHandling: 'merge',
506
+ });
507
+ });
508
+ }
509
+
510
+ private _handleFormChange(forceOverwrite = false) {
511
+ const currentVal = clone(this._currentFormValue$.value);
512
+ const newVal = clone(this._formValue);
513
+
514
+ if (equal(currentVal, newVal)) {
515
+ return;
516
+ }
517
+
518
+ if (forceOverwrite) {
519
+ this._lastFormValue$.next(newVal);
520
+ } else {
521
+ this._lastFormValue$.next(currentVal);
522
+ }
523
+
524
+ this._currentFormValue$.next(newVal);
525
+ }
526
+ }
@@ -0,0 +1,169 @@
1
+ import { FormControl, FormGroup } from '@angular/forms';
2
+ import { QueryField } from './query-form';
3
+
4
+ export interface QueryFieldOptions<T = unknown> {
5
+ /**
6
+ * The default value of the field.
7
+ * Will be set to the initial value of the form control if not specified.
8
+ */
9
+ defaultValue?: T | null;
10
+
11
+ /**
12
+ * Debounce time in milliseconds.
13
+ */
14
+ debounce?: number;
15
+
16
+ /**
17
+ * Whether the debounce should not be applied if the value is falsy.
18
+ * This is useful for fields that can be cleared like a search field.
19
+ *
20
+ * @default false
21
+ */
22
+ disableDebounceIfFalsy?: boolean;
23
+
24
+ /**
25
+ * The form control for the field.
26
+ */
27
+ control: FormControl<T | null>;
28
+
29
+ /**
30
+ * Append the field's value to the url.
31
+ *
32
+ * The field wont be appended to the url if one of the following is true:
33
+ * - It is the default value and `appendDefaultValueToUrl` is `false`
34
+ * - It is an empty string
35
+ * - It is `null`
36
+ * - It is `undefined`
37
+ *
38
+ * @default true
39
+ */
40
+ appendToUrl?: boolean;
41
+
42
+ /**
43
+ * Append the field's value to the url even if it is the default value.
44
+ * Will be ignored if `appendToUrl` is `false`.
45
+ *
46
+ * @default false
47
+ */
48
+ appendDefaultValueToUrl?: boolean;
49
+
50
+ /**
51
+ * Reset the field's value to the default value if one or more of the specified fields are changed.
52
+ *
53
+ * @example
54
+ * // Given this field is a `page` field, the field will be reset if the `limit`, `query` or `search` fields are changed.
55
+ * ['limit', 'query', 'search']
56
+ */
57
+ isResetBy?: string | string[];
58
+
59
+ /**
60
+ * Whether the field should change the count of current active filters.
61
+ *
62
+ * Will be `true` for these common fields:
63
+ * - `page`
64
+ * - `skip`
65
+ * - `take`
66
+ * - `limit`
67
+ * - `sort`
68
+ * - `sortBy`
69
+ * - `sortOrder`
70
+ * - `query`
71
+ * - `search`
72
+ * @default false
73
+ */
74
+ skipInFilterCount?: boolean;
75
+
76
+ /**
77
+ * A function that transforms the the value gotten from the url query params to a value required by the field.
78
+ * E.g. for a number field, the value from the url query params is a string, but the field requires a number.
79
+ *
80
+ * The following common transformations are already implemented:
81
+ * - `transformToString`
82
+ * - `transformToStringArray`
83
+ * - `transformToNumber`
84
+ * - `transformToNumberArray`
85
+ * - `transformToBoolean`
86
+ * - `transformToBooleanArray`
87
+ * - `transformToDate` (expects a string in the default js format)
88
+ * - `transformToDateArray` (expects strings in the default js format)
89
+ * - `transformToSort` (expects a string in the format `field:direction`)
90
+ */
91
+ queryParamToValueTransformFn?: (val: unknown) => T | null;
92
+
93
+ /**
94
+ * A function that transforms the value of the field to a value that can be appended to the url query params.
95
+ *
96
+ * The following common transformations are already implemented:
97
+ * - `transformToSortQueryParam` (returns an object containing `field` and `direction` properties)
98
+ */
99
+ valueToQueryParamTransformFn?: (val: T | null) => unknown;
100
+ }
101
+
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ export type QueryFormGroupControls<T extends Record<string, QueryField<any>>> = {
104
+ [Property in keyof T]: T[Property]['control'];
105
+ };
106
+
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ export type QueryFormValue<T extends Record<string, QueryField<any>>> = {
109
+ [Property in keyof T]: T[Property]['control']['value'];
110
+ };
111
+
112
+ export type QueryFormOf<T extends FormGroup> = {
113
+ [K in keyof T['controls']]: QueryField<T['controls'][K]['value']>;
114
+ };
115
+
116
+ export interface QueryFormGroup extends FormGroup {
117
+ _setValue: FormGroup['setValue'];
118
+ _patchValue: FormGroup['patchValue'];
119
+ setValue(value: unknown, options?: QueryFormWriteOptions): void;
120
+ patchValue(value: unknown, options?: QueryFormWriteOptions): void;
121
+ }
122
+
123
+ export interface QueryFormObserveOptions {
124
+ /**
125
+ * Whether the form value should be synced to the url query params.
126
+ * @default true
127
+ */
128
+ writeToQueryParams?: boolean;
129
+
130
+ /**
131
+ * Whether the form value should be updated when the url query params change.
132
+ * @default true
133
+ */
134
+ syncOnNavigation?: boolean;
135
+
136
+ /**
137
+ * If true, the navigation will not create a new entry in the browser's history.
138
+ * @default false
139
+ */
140
+ replaceUrl?: boolean;
141
+ }
142
+
143
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
+ export interface QueryFormValueEvent<T extends Record<string, QueryField<any>>> {
145
+ previousValue: QueryFormValue<T> | null;
146
+ currentValue: QueryFormValue<T>;
147
+ }
148
+
149
+ export interface QueryFormWriteOptions {
150
+ /**
151
+ * When true, each change only affects this control, and not its parent.
152
+ * @default false
153
+ */
154
+ onlySelf?: boolean;
155
+
156
+ /**
157
+ * When true or not supplied (the default), both the `statusChanges` and `valueChanges`
158
+ * observables emit events with the latest status and value when the control value is updated.
159
+ * When false, no events are emitted.
160
+ * @default true
161
+ */
162
+ emitEvent?: boolean;
163
+
164
+ /**
165
+ * When true, the `resetBy` conditions will be ignored for this write.
166
+ * @default false
167
+ */
168
+ skipResets?: boolean;
169
+ }