@juulsgaard/signal-tools 18.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Frederik Juulsgaard Sørensen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Signal Tools
2
+
3
+ ![NPM License](https://img.shields.io/npm/v/@juulsgaard/signal-tools)
4
+ ![NPM License](https://img.shields.io/npm/l/@juulsgaard/signal-tools)
5
+ ![NPM Downloads](https://img.shields.io/npm/dw/@juulsgaard/signal-tools)
6
+
7
+ A utility library for Angular Signals
8
+
9
+ ## Description
10
+
11
+ This library contains utility methods, types and classes to facilitate complicated operations with Angular Signals.
12
+
13
+ Documentation of all the types, tools and classes can be found in the [Documentation](https://juulsgaard.github.io/signal-tools/)
package/dist/async.js ADDED
@@ -0,0 +1,96 @@
1
+ import { assertInInjectionContext, signal, effect, DestroyRef, inject } from '@angular/core';
2
+
3
+ function debouncedSignal(inputSignal, delay = 500, options) {
4
+ if (options?.injector) assertInInjectionContext(debouncedSignal);
5
+ const output = signal(inputSignal());
6
+ effect((onDestroy) => {
7
+ const value = inputSignal();
8
+ const timeout = setTimeout(() => output.set(value), delay);
9
+ onDestroy(() => clearTimeout(timeout));
10
+ }, { allowSignalWrites: true, injector: options?.injector });
11
+ return output;
12
+ }
13
+ function throttledSignal(inputSignal, delay = 500, options) {
14
+ if (options?.injector) assertInInjectionContext(throttledSignal);
15
+ const output = signal(inputSignal());
16
+ let nextValue = inputSignal();
17
+ let timeout = void 0;
18
+ effect(() => {
19
+ const value = inputSignal();
20
+ nextValue = value;
21
+ if (timeout) return;
22
+ if (options?.emitLeading) {
23
+ output.set(value);
24
+ }
25
+ timeout = setTimeout(() => {
26
+ output.set(nextValue);
27
+ timeout = void 0;
28
+ }, delay);
29
+ }, { allowSignalWrites: true, injector: options?.injector });
30
+ const onDestroy = options?.injector?.get(DestroyRef) ?? inject(DestroyRef);
31
+ onDestroy.onDestroy(() => clearTimeout(timeout));
32
+ return output;
33
+ }
34
+ function debouncedThrottledSignal(inputSignal, throttleDelay = 1e3, debounceDelay = 300, options) {
35
+ if (options?.injector) assertInInjectionContext(debouncedThrottledSignal);
36
+ const output = signal(inputSignal());
37
+ let nextValue = inputSignal();
38
+ let longTimeout = void 0;
39
+ function clearThrottle() {
40
+ if (!longTimeout) return;
41
+ clearTimeout(longTimeout);
42
+ longTimeout = void 0;
43
+ }
44
+ effect((onDestroy2) => {
45
+ const value = inputSignal();
46
+ const shortTimeout = setTimeout(() => {
47
+ output.set(value);
48
+ clearThrottle();
49
+ }, debounceDelay);
50
+ onDestroy2(() => clearTimeout(shortTimeout));
51
+ nextValue = value;
52
+ if (longTimeout) return;
53
+ longTimeout = setTimeout(() => {
54
+ output.set(nextValue);
55
+ longTimeout = void 0;
56
+ }, throttleDelay);
57
+ }, { allowSignalWrites: true, injector: options?.injector });
58
+ const onDestroy = options?.injector?.get(DestroyRef) ?? inject(DestroyRef);
59
+ onDestroy.onDestroy(() => clearTimeout(longTimeout));
60
+ return output;
61
+ }
62
+ function searchSignal(inputSignal, throttleDelay = 1e3, debounceDelay = 300, options) {
63
+ if (options?.injector) assertInInjectionContext(searchSignal);
64
+ const query = signal(inputSignal());
65
+ let nextValue = inputSignal();
66
+ let longTimeout = void 0;
67
+ function clearThrottle() {
68
+ if (!longTimeout) return;
69
+ clearTimeout(longTimeout);
70
+ longTimeout = void 0;
71
+ }
72
+ effect((onDestroy2) => {
73
+ const value = inputSignal();
74
+ if (value === void 0 || value === "") {
75
+ query.set(value);
76
+ clearThrottle();
77
+ return;
78
+ }
79
+ const shortTimeout = setTimeout(() => {
80
+ query.set(value);
81
+ clearThrottle();
82
+ }, debounceDelay);
83
+ onDestroy2(() => clearTimeout(shortTimeout));
84
+ nextValue = value;
85
+ if (longTimeout) return;
86
+ longTimeout = setTimeout(() => {
87
+ query.set(nextValue);
88
+ longTimeout = void 0;
89
+ }, throttleDelay);
90
+ }, { allowSignalWrites: true, injector: options?.injector });
91
+ const onDestroy = options?.injector?.get(DestroyRef) ?? inject(DestroyRef);
92
+ onDestroy.onDestroy(() => clearTimeout(longTimeout));
93
+ return query;
94
+ }
95
+
96
+ export { debouncedSignal, debouncedThrottledSignal, searchSignal, throttledSignal };
@@ -0,0 +1,134 @@
1
+ import { signal, computed, untracked } from '@angular/core';
2
+
3
+ class SignalMap {
4
+ constructor(values) {
5
+ this._map = signal(new Map(values));
6
+ this.value = this._map.asReadonly();
7
+ this.size = computed(() => this.value().size);
8
+ this.empty = computed(() => this.size() <= 0);
9
+ }
10
+ [Symbol.iterator]() {
11
+ return this.value()[Symbol.iterator]();
12
+ }
13
+ getCopy() {
14
+ return new Map(untracked(this.value));
15
+ }
16
+ //<editor-fold desc="Actions">
17
+ /**
18
+ * Remove all keys not in the whitelist
19
+ * @param whitelist - The keys to keep
20
+ */
21
+ filter(whitelist) {
22
+ const length = whitelist && "size" in whitelist ? whitelist.size : whitelist?.length;
23
+ if (!length) {
24
+ return this.clear();
25
+ }
26
+ const whitelistSet = whitelist instanceof Set ? whitelist : new Set(whitelist);
27
+ const map = this.getCopy();
28
+ for (let [key] of untracked(this.value)) {
29
+ if (whitelistSet.has(key)) continue;
30
+ map.delete(key);
31
+ }
32
+ if (untracked(this.size) !== map.size) {
33
+ this._map.set(map);
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+ /** Clear the collection */
39
+ clear() {
40
+ if (!untracked(this.size)) return false;
41
+ this._map.set(/* @__PURE__ */ new Map());
42
+ return true;
43
+ }
44
+ /**
45
+ * Add a value to the collection if the key is not in use
46
+ * @param key - The key to use
47
+ * @param value - The value to add
48
+ * @return added - Returns true if the value was added
49
+ */
50
+ add(key, value) {
51
+ if (untracked(this.value).has(key)) return false;
52
+ const map = this.getCopy();
53
+ map.set(key, value);
54
+ this._map.set(map);
55
+ return true;
56
+ }
57
+ /**
58
+ * Add a value to the collection
59
+ * @param key - The key to use
60
+ * @param value - The value to add
61
+ * @return added - Returns true if the value was added or changed
62
+ */
63
+ set(key, value) {
64
+ const state = untracked(this.value);
65
+ if (state.has(key) && state.get(key) === value) return false;
66
+ const map = this.getCopy();
67
+ map.set(key, value);
68
+ this._map.set(map);
69
+ return true;
70
+ }
71
+ /**
72
+ * Remove a key from the collection
73
+ * @param key - The key to remove
74
+ * @return removed - True if an item was removed
75
+ */
76
+ delete(key) {
77
+ if (!untracked(this.value).has(key)) return false;
78
+ const map = this.getCopy();
79
+ map.delete(key);
80
+ this._map.set(map);
81
+ return true;
82
+ }
83
+ /**
84
+ * Remove keys from the collection
85
+ * @param values - The keys to remove
86
+ * @return removed - True if items were removed
87
+ */
88
+ deleteRange(values) {
89
+ const map = this.getCopy();
90
+ const size = map.size;
91
+ values.forEach((v) => map.delete(v));
92
+ if (map.size === size) return false;
93
+ this._map.set(map);
94
+ return true;
95
+ }
96
+ /**
97
+ * Toggle a value in the map
98
+ * @param key - The key to toggle
99
+ * @param value - The value to insert if applicable
100
+ * @param state - A forced state (`true` = always add, `false` = always delete)
101
+ * @returns The applied change (`true` = item added, `false` = item removed, `undefined` = nothing changed)
102
+ */
103
+ toggle(key, value, state) {
104
+ if (untracked(this.value).has(key)) {
105
+ if (state === true) return void 0;
106
+ const map2 = this.getCopy();
107
+ map2.delete(key);
108
+ this._map.set(map2);
109
+ return false;
110
+ }
111
+ if (state === false) return void 0;
112
+ const map = this.getCopy();
113
+ map.set(key, value);
114
+ this._map.set(map);
115
+ return true;
116
+ }
117
+ /** Manually modify the inner collection */
118
+ modify(modify) {
119
+ const map = this.getCopy();
120
+ modify(map);
121
+ this._map.set(map);
122
+ }
123
+ /** @inheritDoc */
124
+ has(key) {
125
+ return computed(() => this.value().has(key));
126
+ }
127
+ /** @inheritDoc */
128
+ get(key) {
129
+ return computed(() => this.value().get(key));
130
+ }
131
+ //</editor-fold>
132
+ }
133
+
134
+ export { SignalMap };
@@ -0,0 +1,143 @@
1
+ import { signal, computed, untracked } from '@angular/core';
2
+
3
+ class SignalSet {
4
+ constructor(values) {
5
+ this._set = signal(new Set(values));
6
+ this.value = this._set.asReadonly();
7
+ this.size = computed(() => this.value().size);
8
+ this.empty = computed(() => this.size() <= 0);
9
+ this.array = computed(() => Array.from(this.value()));
10
+ }
11
+ /** @inheritDoc */
12
+ [Symbol.iterator]() {
13
+ return this.value()[Symbol.iterator]();
14
+ }
15
+ getCopy() {
16
+ return new Set(untracked(this.value));
17
+ }
18
+ //<editor-fold desc="Actions">
19
+ /**
20
+ * Remove all keys not in the whitelist
21
+ * @param whitelist - The keys to keep
22
+ */
23
+ filter(whitelist) {
24
+ const length = whitelist && "size" in whitelist ? whitelist.size : whitelist?.length;
25
+ if (!length) {
26
+ return this.clear();
27
+ }
28
+ const whitelistSet = whitelist instanceof Set ? whitelist : new Set(whitelist);
29
+ const set = this.getCopy();
30
+ for (let value of untracked(this.value)) {
31
+ if (whitelistSet.has(value)) continue;
32
+ set.delete(value);
33
+ }
34
+ if (untracked(this.size) !== set.size) {
35
+ this._set.set(set);
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ /** Clear the collection */
41
+ clear() {
42
+ if (!untracked(this.size)) return false;
43
+ this._set.set(/* @__PURE__ */ new Set());
44
+ return true;
45
+ }
46
+ /**
47
+ * Add a value to the collection is not already present
48
+ * @param value - The value to add
49
+ * @return added - Returns true if the value was added
50
+ */
51
+ add(value) {
52
+ if (untracked(this.value).has(value)) return false;
53
+ const set = this.getCopy();
54
+ set.add(value);
55
+ this._set.set(set);
56
+ return true;
57
+ }
58
+ /**
59
+ * Add a list of values to the collection is not already present
60
+ * @param values - The values to add
61
+ * @return added - Returns true if any value was added
62
+ */
63
+ addRange(values) {
64
+ const set = this.getCopy();
65
+ const size = set.size;
66
+ values.forEach((v) => set.add(v));
67
+ if (set.size === size) return false;
68
+ this._set.set(set);
69
+ return true;
70
+ }
71
+ /**
72
+ * Reset the values in the collection to the provided list
73
+ * @param values - The values to set
74
+ * @return changed - Returns true if the collection changed
75
+ */
76
+ set(values = []) {
77
+ if (!values.length && !this.size) return false;
78
+ if (values.length === untracked(this.size)) {
79
+ const same = values.every((x) => this.has(x));
80
+ if (same) return false;
81
+ }
82
+ this._set.set(new Set(values));
83
+ return true;
84
+ }
85
+ /**
86
+ * Remove a value from the collection
87
+ * @param value - The key to remove
88
+ * @return removed - True if the value existed
89
+ */
90
+ delete(value) {
91
+ if (!untracked(this.value).has(value)) return false;
92
+ const set = this.getCopy();
93
+ set.delete(value);
94
+ this._set.set(set);
95
+ return true;
96
+ }
97
+ /**
98
+ * Remove values from the collection
99
+ * @param values - The values to remove
100
+ * @return removed - True if any values were removed
101
+ */
102
+ deleteRange(values) {
103
+ const set = this.getCopy();
104
+ const size = set.size;
105
+ values.forEach((v) => set.delete(v));
106
+ if (set.size === size) return false;
107
+ this._set.set(set);
108
+ return true;
109
+ }
110
+ /**
111
+ * Toggle a value in the set
112
+ * @param value - The value to toggle
113
+ * @param state - A forced state (`true` = always add, `false` = always delete)
114
+ * @returns The applied change (`true` = item added, `false` = item removed, `undefined` = nothing changed)
115
+ */
116
+ toggle(value, state) {
117
+ if (untracked(this.value).has(value)) {
118
+ if (state === true) return void 0;
119
+ const set2 = this.getCopy();
120
+ set2.delete(value);
121
+ this._set.set(set2);
122
+ return false;
123
+ }
124
+ if (state === false) return void 0;
125
+ const set = this.getCopy();
126
+ set.add(value);
127
+ this._set.set(set);
128
+ return true;
129
+ }
130
+ /** Manually modify the inner collection */
131
+ modify(modify) {
132
+ const set = this.getCopy();
133
+ modify(set);
134
+ this._set.set(set);
135
+ }
136
+ /** @inheritDoc */
137
+ has(value) {
138
+ return computed(() => this.value().has(value));
139
+ }
140
+ //</editor-fold>
141
+ }
142
+
143
+ export { SignalSet };
@@ -0,0 +1,287 @@
1
+ import { Signal, Injector } from '@angular/core';
2
+ import { BehaviorSubject, Subject, Subscribable } from 'rxjs';
3
+ import { IValueLoadingState } from '@juulsgaard/rxjs-tools';
4
+
5
+ /**
6
+ * Create a signal where values are only emitted after the values have settled for `delay` ms
7
+ * @category Async Signals
8
+ * @param inputSignal - The input value to debounce
9
+ * @param delay - The required delay between values
10
+ * @param options - Additional options
11
+ */
12
+ declare function debouncedSignal<T>(inputSignal: Signal<T>, delay?: number, options?: {
13
+ injector?: Injector;
14
+ }): Signal<T>;
15
+ /**
16
+ * Create a signal where values are throttled
17
+ * @category Async Signals
18
+ * @param inputSignal - The input value to throttle
19
+ * @param delay - The delay between values
20
+ * @param options - Additional options
21
+ */
22
+ declare function throttledSignal<T>(inputSignal: Signal<T>, delay?: number, options?: {
23
+ injector?: Injector;
24
+ emitLeading?: boolean;
25
+ }): Signal<T>;
26
+ /**
27
+ * Create a signal where values are throttled and debounced
28
+ * @category Async Signals
29
+ * @param inputSignal - The input value to throttle and debounce
30
+ * @param throttleDelay - The delay between value emissions during continual input
31
+ * @param debounceDelay - The amount of time the input needs to settle before a value is emitted
32
+ * @param options - Additional options
33
+ */
34
+ declare function debouncedThrottledSignal<T extends string | undefined>(inputSignal: Signal<T>, throttleDelay?: number, debounceDelay?: number, options?: {
35
+ injector?: Injector;
36
+ }): Signal<T>;
37
+ /**
38
+ * Create a signal where values are throttled and debounced, but where empty strings skip the delay
39
+ * @category Async Signals
40
+ * @param inputSignal - The input value to throttle and debounce
41
+ * @param throttleDelay - The delay between value emissions during continual input
42
+ * @param debounceDelay - The amount of time the input needs to settle before a value is emitted
43
+ * @param options - Additional options
44
+ */
45
+ declare function searchSignal<T extends string | undefined>(inputSignal: Signal<T>, throttleDelay?: number, debounceDelay?: number, options?: {
46
+ injector?: Injector;
47
+ }): Signal<T>;
48
+
49
+ /**
50
+ * A simple conversion of Behaviour Subject to signal.
51
+ * This will not handle unsubscription, and should only be used when the subject itself is closed / unloaded properly.
52
+ * @category Interop
53
+ * @param subject
54
+ */
55
+ declare function subjectToSignal<T>(subject: BehaviorSubject<T>): Signal<T>;
56
+ /**
57
+ * A simple conversion of Subject to signal.
58
+ * This will not handle unsubscription, and should only be used when the subject itself is closed / unloaded properly.
59
+ * @category Interop
60
+ * @param subject
61
+ * @param initial - An initial value for the signal.
62
+ */
63
+ declare function subjectToSignal<T>(subject: Subject<T>, initial: T): Signal<T>;
64
+
65
+ /** @category Loading Signal */
66
+ type LoadingSignal<T> = Signal<T | undefined> & {
67
+ /** The current error state */
68
+ readonly error: Signal<Error | undefined>;
69
+ /** The current loading state */
70
+ readonly loading: Signal<boolean>;
71
+ /** Dispose the underlying request / action */
72
+ readonly dispose: () => void;
73
+ };
74
+ /**
75
+ * Options for creating a LoadingSignal
76
+ * @category Loading Signal
77
+ */
78
+ interface CreateLoadingSignalOptions {
79
+ /** The loading state */
80
+ loading?: Signal<boolean>;
81
+ /** The error state */
82
+ error?: Signal<Error | undefined>;
83
+ /** An optional disposal method */
84
+ dispose?: () => void;
85
+ }
86
+ /**
87
+ * Create a Loading Signal
88
+ * @param value - The value signal
89
+ * @param options - Optional state data
90
+ * @category Loading Signal
91
+ * @constructor
92
+ */
93
+ declare function loadingSignal<T>(value: Signal<T | undefined>, options?: CreateLoadingSignalOptions): LoadingSignal<T>;
94
+ /** @category Loading Signal */
95
+ interface LoadingSignalOptions {
96
+ /** Dispose the loading signal on destroy */
97
+ autoDispose?: boolean;
98
+ /** Pass an injector for running outside of injection context */
99
+ injector?: Injector;
100
+ }
101
+ /**
102
+ * Convert a LoadingState to a LoadingSignal
103
+ * @param loadingState
104
+ * @param options
105
+ * @category Loading Signal
106
+ */
107
+ declare function toLoadingSignal<T>(loadingState: IValueLoadingState<T>, options?: LoadingSignalOptions): LoadingSignal<T>;
108
+ /**
109
+ * Convert a Subscribable to a LoadingSignal
110
+ * @param observable
111
+ * @param options
112
+ * @category Loading Signal
113
+ */
114
+ declare function toLoadingSignal<T>(observable: Subscribable<T>, options?: LoadingSignalOptions): LoadingSignal<T>;
115
+ /**
116
+ * Convert a promise to a LoadingSignal
117
+ * @param promise
118
+ * @param options
119
+ * @category Loading Signal
120
+ */
121
+ declare function toLoadingSignal<T>(promise: Promise<T>, options?: LoadingSignalOptions): LoadingSignal<T>;
122
+
123
+ /**
124
+ * A Set where the state and values are accessible as signals
125
+ * @category Signal Collections
126
+ */
127
+ declare class SignalSet<T> implements ReadonlySignalSet<T> {
128
+ private readonly _set;
129
+ /** @inheritDoc */
130
+ readonly value: Signal<ReadonlySet<T>>;
131
+ /** @inheritDoc */
132
+ readonly size: Signal<number>;
133
+ /** @inheritDoc */
134
+ readonly empty: Signal<boolean>;
135
+ /** @inheritDoc */
136
+ readonly array: Signal<T[]>;
137
+ constructor(values?: T[]);
138
+ /** @inheritDoc */
139
+ [Symbol.iterator](): IterableIterator<T>;
140
+ private getCopy;
141
+ /**
142
+ * Remove all keys not in the whitelist
143
+ * @param whitelist - The keys to keep
144
+ */
145
+ filter(whitelist: T[] | ReadonlySet<T> | undefined): boolean;
146
+ /** Clear the collection */
147
+ clear(): boolean;
148
+ /**
149
+ * Add a value to the collection is not already present
150
+ * @param value - The value to add
151
+ * @return added - Returns true if the value was added
152
+ */
153
+ add(value: T): boolean;
154
+ /**
155
+ * Add a list of values to the collection is not already present
156
+ * @param values - The values to add
157
+ * @return added - Returns true if any value was added
158
+ */
159
+ addRange(values: T[]): boolean;
160
+ /**
161
+ * Reset the values in the collection to the provided list
162
+ * @param values - The values to set
163
+ * @return changed - Returns true if the collection changed
164
+ */
165
+ set(values?: T[]): boolean;
166
+ /**
167
+ * Remove a value from the collection
168
+ * @param value - The key to remove
169
+ * @return removed - True if the value existed
170
+ */
171
+ delete(value: T): boolean;
172
+ /**
173
+ * Remove values from the collection
174
+ * @param values - The values to remove
175
+ * @return removed - True if any values were removed
176
+ */
177
+ deleteRange(values: T[]): boolean;
178
+ /**
179
+ * Toggle a value in the set
180
+ * @param value - The value to toggle
181
+ * @param state - A forced state (`true` = always add, `false` = always delete)
182
+ * @returns The applied change (`true` = item added, `false` = item removed, `undefined` = nothing changed)
183
+ */
184
+ toggle(value: T, state?: boolean): boolean | undefined;
185
+ /** Manually modify the inner collection */
186
+ modify(modify: (set: Set<T>) => void): void;
187
+ /** @inheritDoc */
188
+ has(value: T): Signal<boolean>;
189
+ }
190
+ /**
191
+ * An immutable Set where the state and values are accessible as signals
192
+ * @category Signal Collections
193
+ */
194
+ interface ReadonlySignalSet<T> extends Iterable<T> {
195
+ /** A signal emitting the number of items in the collection */
196
+ readonly size: Signal<number>;
197
+ /** A signal emitting true if the collection is empty */
198
+ readonly empty: Signal<boolean>;
199
+ /** A signal emitting the inner non-signal Set */
200
+ readonly value: Signal<ReadonlySet<T>>;
201
+ /** A signal emitting the values of the set as an array */
202
+ readonly array: Signal<ReadonlyArray<T>>;
203
+ /** Create a signal emitting true if a key exists in the collection */
204
+ has(key: T): Signal<boolean>;
205
+ }
206
+
207
+ /**
208
+ * A Map where the state and values are accessible as signals
209
+ * @category Signal Collections
210
+ */
211
+ declare class SignalMap<TKey, TVal> implements ReadonlySignalMap<TKey, TVal> {
212
+ private readonly _map;
213
+ /** @inheritDoc */
214
+ readonly value: Signal<ReadonlyMap<TKey, TVal>>;
215
+ /** @inheritDoc */
216
+ readonly size: Signal<number>;
217
+ /** @inheritDoc */
218
+ readonly empty: Signal<boolean>;
219
+ constructor(values?: ReadonlyMap<TKey, TVal>);
220
+ [Symbol.iterator](): IterableIterator<[TKey, TVal]>;
221
+ private getCopy;
222
+ /**
223
+ * Remove all keys not in the whitelist
224
+ * @param whitelist - The keys to keep
225
+ */
226
+ filter(whitelist: ReadonlyArray<TKey> | ReadonlySet<TKey> | undefined): boolean;
227
+ /** Clear the collection */
228
+ clear(): boolean;
229
+ /**
230
+ * Add a value to the collection if the key is not in use
231
+ * @param key - The key to use
232
+ * @param value - The value to add
233
+ * @return added - Returns true if the value was added
234
+ */
235
+ add(key: TKey, value: TVal): boolean;
236
+ /**
237
+ * Add a value to the collection
238
+ * @param key - The key to use
239
+ * @param value - The value to add
240
+ * @return added - Returns true if the value was added or changed
241
+ */
242
+ set(key: TKey, value: TVal): boolean;
243
+ /**
244
+ * Remove a key from the collection
245
+ * @param key - The key to remove
246
+ * @return removed - True if an item was removed
247
+ */
248
+ delete(key: TKey): boolean;
249
+ /**
250
+ * Remove keys from the collection
251
+ * @param values - The keys to remove
252
+ * @return removed - True if items were removed
253
+ */
254
+ deleteRange(values: TKey[]): boolean;
255
+ /**
256
+ * Toggle a value in the map
257
+ * @param key - The key to toggle
258
+ * @param value - The value to insert if applicable
259
+ * @param state - A forced state (`true` = always add, `false` = always delete)
260
+ * @returns The applied change (`true` = item added, `false` = item removed, `undefined` = nothing changed)
261
+ */
262
+ toggle(key: TKey, value: TVal, state?: boolean): boolean | undefined;
263
+ /** Manually modify the inner collection */
264
+ modify(modify: (map: Map<TKey, TVal>) => void): void;
265
+ /** @inheritDoc */
266
+ has(key: TKey): Signal<boolean>;
267
+ /** @inheritDoc */
268
+ get(key: TKey): Signal<TVal | undefined>;
269
+ }
270
+ /**
271
+ * An immutable Map where the state and values are accessible as signals
272
+ * @category Signal Collections
273
+ */
274
+ interface ReadonlySignalMap<TKey, TVal> extends Iterable<[TKey, TVal]> {
275
+ /** A signal emitting the number of items in the collection */
276
+ readonly size: Signal<number>;
277
+ /** A signal emitting true if the collection is empty */
278
+ readonly empty: Signal<boolean>;
279
+ /** A signal emitting the inner non-signal map */
280
+ readonly value: Signal<ReadonlyMap<TKey, TVal>>;
281
+ /** Create a signal emitting true if a key exists in the collection */
282
+ has(key: TKey): Signal<boolean>;
283
+ /** Create a signal emitting the value under a given key */
284
+ get(key: TKey): Signal<TVal | undefined>;
285
+ }
286
+
287
+ export { type CreateLoadingSignalOptions, type LoadingSignal, type LoadingSignalOptions, type ReadonlySignalMap, type ReadonlySignalSet, SignalMap, SignalSet, debouncedSignal, debouncedThrottledSignal, loadingSignal, searchSignal, subjectToSignal, throttledSignal, toLoadingSignal };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { debouncedSignal, debouncedThrottledSignal, searchSignal, throttledSignal } from './async.js';
2
+ export { subjectToSignal } from './interop.js';
3
+ export { loadingSignal, toLoadingSignal } from './loading-signal.js';
4
+ export { SignalSet } from './data-structures/signal-set.js';
5
+ export { SignalMap } from './data-structures/signal-map.js';
@@ -0,0 +1,11 @@
1
+ import { signal } from '@angular/core';
2
+ import { BehaviorSubject } from 'rxjs';
3
+
4
+ function subjectToSignal(subject, initial) {
5
+ const init = subject instanceof BehaviorSubject ? subject.value : initial;
6
+ const sig = signal(init);
7
+ subject.subscribe((val) => sig.set(val));
8
+ return sig;
9
+ }
10
+
11
+ export { subjectToSignal };
@@ -0,0 +1,68 @@
1
+ import { signal, assertInInjectionContext, DestroyRef, inject } from '@angular/core';
2
+ import { ILoadingState } from '@juulsgaard/rxjs-tools';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
4
+ import { parseError } from '@juulsgaard/ts-tools';
5
+
6
+ function loadingSignal(value, options) {
7
+ const loadingSignal2 = value;
8
+ loadingSignal2.loading = options?.loading ?? signal(false);
9
+ loadingSignal2.error = options?.error ?? signal(void 0);
10
+ loadingSignal2.dispose = options?.dispose ?? (() => void 0);
11
+ return loadingSignal2;
12
+ }
13
+ function toLoadingSignal(loadingState, options) {
14
+ const injector = options?.injector;
15
+ const needsInjector = !!options?.autoDispose;
16
+ if (needsInjector && !injector) assertInInjectionContext(toLoadingSignal);
17
+ let state;
18
+ if (loadingState instanceof ILoadingState) {
19
+ state = fromLoadingState(loadingState);
20
+ } else if (loadingState instanceof Promise) {
21
+ state = fromPromise(loadingState);
22
+ } else {
23
+ state = fromObservable(loadingState);
24
+ }
25
+ if (options?.autoDispose) {
26
+ const destroy = injector?.get(DestroyRef) ?? inject(DestroyRef);
27
+ destroy.onDestroy(() => state.dispose());
28
+ }
29
+ return state;
30
+ }
31
+ function fromObservable(value$) {
32
+ const value = signal(void 0);
33
+ const loading = signal(true);
34
+ const error = signal(void 0);
35
+ const sub = value$.subscribe({
36
+ next: (val) => {
37
+ sub.unsubscribe();
38
+ loading.set(false);
39
+ value.set(val);
40
+ },
41
+ error: (err) => {
42
+ sub.unsubscribe();
43
+ loading.set(false);
44
+ error.set(parseError(err));
45
+ },
46
+ complete: () => {
47
+ sub.unsubscribe();
48
+ loading.set(false);
49
+ }
50
+ });
51
+ return loadingSignal(value, { loading, error, dispose: () => sub.unsubscribe() });
52
+ }
53
+ function fromPromise(promise) {
54
+ const value = signal(void 0);
55
+ const loading = signal(true);
56
+ const error = signal(void 0);
57
+ promise.then((val) => value.set(val), (err) => error.set(parseError(err)));
58
+ promise.finally(() => loading.set(false));
59
+ return loadingSignal(value, { loading, error });
60
+ }
61
+ function fromLoadingState(state) {
62
+ const value = toSignal(state.result$, { manualCleanup: true });
63
+ const loading = toSignal(state.loading$, { manualCleanup: true, initialValue: true });
64
+ const error = toSignal(state.error$, { manualCleanup: true });
65
+ return loadingSignal(value, { loading, error, dispose: () => state.cancel() });
66
+ }
67
+
68
+ export { loadingSignal, toLoadingSignal };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@juulsgaard/signal-tools",
3
+ "version": "18.0.0",
4
+ "description": "Tools for working with Angular signals",
5
+ "repository": "git@github.com:Juulsgaard/signal-tools.git",
6
+ "homepage": "https://juulsgaard.github.io/signal-tools/",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "type": "module",
10
+ "sideEffects": false,
11
+ "author": "Frederik Sørensen",
12
+ "license": "MIT",
13
+ "files": [
14
+ "dist/**"
15
+ ],
16
+ "peerDependencies": {
17
+ "@angular/core": "^18.2.5",
18
+ "@juulsgaard/ts-tools": "^1.6.4",
19
+ "@juulsgaard/rxjs-tools": "^1.7.4",
20
+ "rxjs": "^7.8.1",
21
+ "tslib": "^2.7.0",
22
+ "typescript": "~5.5.4"
23
+ },
24
+ "devDependencies": {
25
+ "@angular/core": "18.2.5",
26
+ "@juulsgaard/rxjs-tools": "1.7.4",
27
+ "@types/jest": "^29.5.12",
28
+ "jest": "^29.7.0",
29
+ "rollup": "^4.19.1",
30
+ "rollup-plugin-dts": "^6.1.1",
31
+ "rollup-plugin-esbuild": "^6.1.1",
32
+ "rxjs": "7.8.1",
33
+ "ts-jest": "^29.2.3",
34
+ "typedoc": "^0.26.7",
35
+ "typescript": "~5.5.4",
36
+ "tslib": "2.7.0"
37
+ },
38
+ "scripts": {
39
+ "build": "rollup -c",
40
+ "test": "jest --config jestconfig.json",
41
+ "prepublishOnly": "rollup -c"
42
+ },
43
+ "packageManager": "pnpm@9.11.0"
44
+ }