@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 +21 -0
- package/README.md +13 -0
- package/dist/async.js +96 -0
- package/dist/data-structures/signal-map.js +134 -0
- package/dist/data-structures/signal-set.js +143 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +5 -0
- package/dist/interop.js +11 -0
- package/dist/loading-signal.js +68 -0
- package/package.json +44 -0
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
|
+

|
|
4
|
+

|
|
5
|
+

|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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';
|
package/dist/interop.js
ADDED
|
@@ -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
|
+
}
|