Add Angular v19 to the list of supported versions in dependencies.
Callbacks onSuccess()
and onError()
were called before $items
was updated. $items()
inside the callbacks had a non-updated value.
New fields:
$lastReadError
$lastReadOneError
$lastReadManyError
$lastRefreshError
In params structures, fields readRequest
and refreshRequest
are deprecated (will be removed in v5).
Use read
and refresh
instead.
New method: fetchItem()
!
This method will check if the item exists in the collection and return it if it does.
If the item does not exist, the request
argument will be used to fetch the item and add it to the collection.
If the option fetchItemRequestFactory
is set, the request
argument is optional.
If both are missing, the resulting Observable will throw an error.
createEffect.forValue()
renamed to getEffectFor()
.
Experimental method for createEffect()
: forValue()
, which takes a value and returns an observable that will execute the effect when subscribed.
- New (experimental!) methods:
readFrom
andreadManyFrom
. Can be called as part of constructor options. EffectFnMethods
renamed toEffectObservables
, and lost methodsnext
,error
andcomplete
- the same functionality with a less ambiguous API can be achieved withEffectListeners
. This API is considered stable now.
- Use
untracked()
every time when reactive context should not be affected; - Use
take(1)
instead offirst()
to preventno elements in sequence
exception.
Improved API for createEffect()
listeners, introduced in v4.1.1.
Methods of the function, returned by createEffect()
:
export type EffectFnMethods = {
next: (fn: ((v: unknown) => void)) => void,
error: (fn: ((v: unknown) => void)) => void,
complete: (fn: (() => void)) => void,
next$: Observable<unknown>,
error$: Observable<unknown>,
};
Also, you can set next
listener or an object with listeners as a second argument, when you call an effect:
class Component {
store = inject(Store);
dialog = inject(Dialog);
toasts = inject(Toasts);
changeZipCode(zipCode: string) {
this.store.changeZipCode(zipCode, () => this.dialog.close());
// or:
this.store.changeZipCode(zipCode, {
next: () => this.dialog.close(),
error: () => this.toasts.error('Error, please try again.'),
});
}
}
createEffect()
now returns not just a function, but a function with methods! :)
API is experimental and might change, so it's documented only here for now.
In your store:
import { createEffect } from './create-effect';
class Store extends Collection<Item> {
readonly changeZipCode = createEffect<string>(_ => _.pipe(
// code to change zipcode
));
}
In your component:
class Component {
store = inject(Store);
dialog = inject(Dialog);
changeZipCode(zipCode: string) {
this.store.changeZipCode.nextValue(() => this.dialog.close());
this.store.changeZipCode(zipCode);
}
}
In this example, the dialog window will be closed only after the service response, and only if it was successful.
Alongside nextValue, there are other methods:
export type EffectFnMethods = {
nextValue: (fn: ((v: unknown) => void)) => void,
nextError: (fn: ((v: unknown) => void)) => void,
onNextValue(): Observable<unknown>,
onNextError(): Observable<unknown>,
};
Internally, values and errors will not be saved in memory if you don't use these methods.
Sometimes we know in advance the IDs of items we read, and it can be quite useful to know that these items are being read.
Now, the methods read()
, readOne()
, and readMany()
accept a parameter item
/items
, where you can pass partial items:
coll.read({
request: req,
items: [{id: 1}, {id: 2}]
});
This will instantly add {id: 1}
and {id: 2}
to $readingItems
, but not to $items
(because they are not in the collection yet).
Params should be objects that have at least the ID field (the field or multiple fields that the comparator will use to find the item). The object can also have any other fields - they will be ignored.
A new method, isItemReading()
, will return a Signal<boolean>
- you can check (reactively) if an item with a specific ID is being read.
The method isItemProcessing()
will now also look for an item in $readingItems
(in addition to previous states).
And a new helper method to quickly convert an array of IDs into partial items:
idsToPartialItems(ids: unknown[], field: string): Partial<T>[]
Angular v18 is now supported.
- Function, returned by
createEffect()
now acceptsSignal<T> | WritableSignal<T>
as an argument; createEffect()
now has optional configuration argument of typeCreateEffectOptions
. Here you can pass an injector, configure if function should retry on error (true by default), and passretry()
configuration options. All fields are optional.
Because of PR#53446, one custom equality check function is restored. You can import it as equalPrimitives()
.
- Angular v17.1.0-next is supported;
- Minimum supported version of Angular is v17.0.0 (stable).
- Status pipes removed: it's quite easy to read status from the collection directly in the template;
- Custom equality functions for Angular Signals removed: this library only operates on immutable data structures, and was using these functions only to guarantee updates even when items were mutated outside. In Angular v17 custom equality functions are ignored for mutable structures, so they are useless even as a tool for other parts of your application;
getTrackByFieldFn()
helper is removed: with Angular v17 built-in control flow, it is not needed anymore;setAfterFirstReadHandler()
is removed: usesetOnFirstItemsRequest()
.
$updatingItems
,$deletingItems
,$refreshingItems
,$mutatingItems
,$processingItems
will only contain items that currently exist in the$items
list. Previously, they could potentially contain non-existing items for a short time. For example, ifread()
ordelete()
operations were executed faster thanupdate()
, andupdate()
was started earlier, thanupdate()
would contain items that were removed bydelete()
, until its (update()
) request is not completed. It was quite difficult to achieve (and even more difficult to notice), but now it's fixed;$mutatingItems
and$processingItems
now contain unique items only. Previously, it was theoretically possible to have duplicates there if some items were being removed and updated simultaneously.
Method getItemByField()
now accepts Signal<T|undefined>
as fieldValue
.
- Method
setAfterFirstReadHandler()
renamed tosetOnFirstItemsRequest()
. Previous method is not removed, but deprecated. - Now it's possible to set onFirstItemsRequest in the constructor (using the options object).
Fix: getItem()
and getItemByField()
should trigger lazy-loading.
Previously, you could load the first set of items into the collection when collection is initialized, or when your service/component decide to do it.
Now, you can also use lazy loading, using the setAfterFirstReadHandler(handlerFn)
- handlerFn
will be called once (asynchronously), when $items
signal is read for the first time.
Example:
class ExampleService {
private readonly coll = new Collection();
private readonly api = inject(ApiService);
private readonly load = createEffect(_ => _.pipe(
switchMap(() => this.coll.read({
request: this.api.getItems()
}))
));
constructor() {
this.coll.setAfterFirstReadHandler(() => this.load());
}
}
It is just an example - it's up to you how your handler will load the first set of items.
As preparation for this change in Angular Signals, collection.$items
now uses the first version of Angular Signals' default equality function. This function will always treat items as non-equal, so the $items
signal will send a notification even if items still point to the same objects after the collection was mutated. This library treats items as immutable structures and will not compare them.
Allow getItem()
to accept undefined
as input (return type has not been changed).
Workaround for Angular Signals issue #51812.
- New state field:
$isBeforeFirstRead: Signal<boolean>
.
Initialized with 'true', will be set tofalse
after the first execution ofread()
,readOne()
, orreadMany()
.
It is designed to be used with 'No entries found' placeholders. effect()
helper was renamed tocreateEffect()
to don't conflict with Angular'seffect()
function.
createEffect()
is still exported aseffect()
for backwards compatibility, and assideEffect()
to don't conflict with NgRx.Store'screateEffect()
.
effect()
helper will now resubscribe on errors, so if you forgot to catch an error - it's not an issue anymore.
Also, if a value, passed to the effect, is an observable, and this observable throws an error, observable will be resubscribed automatically.
getItem()
and getItemByField()
now accept equalFn
parameter - you can set your own equality check function or set undefined
to use the default one.
- Specialized equality check functions will be used in
getItem()
andgetItemByField()
. - Better documentation and more tests for equality check functions.
- Only update status$ signals when needed.
signalEqual
object is exposed as public API - here you can find functions to use for custom equality checks.
Better equality functions for signals.
- New method to replace previously removed
postInit()
:asyncInit()
. Will be called in the next microtask from the constructor (init()
will be called first). Collection.constructor()
will complain in dev mode, if the comparator has to use default id fields, because no custom id fields are provided and no custom comparator is provided - that's exactly whyCollection
is not@Injectable
anymore: providing this information is critically important for the correct functioning ofCollection
, so comparator fields (or a custom comparator) should be set explicitly. This error will help you not forget about it but will not pollute the console in production mode.
New helper: effect()
function.
Copy of effect()
method of NgRx ComponentStore, where takeUntil(this.destroy$)
is replaced with takeUntilDestroyed(destroyRef)
, to use it as a function.
Breaking changes:
- Observable-based version removed;
CollectionCore
renamed toCollectionInterface
;- Fields, containing signals now prefixed with '$' (
$items
,$totalCountFetched
,$isUpdating
and so on); - Methods don't accept observables anymore:
-
isItemDeleting()
-
isItemRefreshing()
-
isItemUpdating()
-
isItemMutating()
-
isItemProcessing()
Collection
class is not@Injectable
anymore. Easiest way to create an injectable class is to extendCollection
with an@Injectable
class;NGX_COLLECTION_OPTIONS
token removed - set options usingconstructor()
orsetOptions()
;- Default value for
onDuplicateErrCallbackParam
changed from{status: 409}
toDuplicateError
object; postInit()
method removed - you can declare your own and call it asPromise.resolve().then(() => this.postInit());
frominit()
if needed;CollectionManager
merged back toCollection
.
getItemByPartial()
is now part of the API.
- Signal-based collection:
processingItems: Signal<T[]>
field has been added; - Observable-based collection:
processingItems$: Observable<T[]>
,processingItemsSignal: Signal<T[]>
fields have been added.
- New methods, for both versions:
listenForItemsUpdate()
,listenForItemsDeletion()
; - Methods
isItemDeleting()
,isItemRefreshing()
,isItemUpdating()
,isItemMutating()
,isItemProcessing()
now acceptPartial<T>
as an argument...; - ...and implemented in the observable-based version. They return signals there as well to have the same API. To get the same result using observables only, you can use
hasItemIn()
method.
Signal-based version (class Collection
) now uses NGX_COLLECTION_OPTIONS
injection token to inject options, because string injection tokens are deprecated in Angular.
- Signal-based Collection!
- API documentation moved to the interfaces.
toObservable()
from '@angular/core/rxjs-interop' will be used without replacement;- If instantiated not in an injection context and without
injector
argument, theconstructor()
will throw an error in development mode or will print toconsole.error
(orerrorReporter
, if set), at runtime.
If class is instantiated in an injection context, the injector
will be created from this context.
- Every
request
parameter now accepts signals! The signal will be read once, at the moment of request execution; - Signals are accepted also by:
-
getItemViewModel()
-
getItem()
-
getItemByField()
- Add support for Angular Signals;
- This version requires Angular 16;
- Export
ViewModel
andItemViewModel
types; - Every
interface
is converted to atype
, so they can now be imported usingimport type
.
Accept Angular 16 as peerDependency.
delete()
anddeleteMany()
now have 2 new optional fields in their params:readRequest
: consecutiveread()
request (will completely resetitems
);decrementTotalCount
: decrementtotalCountFetched
by count of removed items, by number, or read new value from the response object.
- Angular and NgRx versions bump, RxJS 7 & 8 versions are supported.
You can now get an observable to be notified about the mutations:
listenForCreate()
listenForRead()
listenForUpdate()
listenForDelete()
Events will be only emitted if there are active observers.
- Comparator now accepts dot-separated paths;
- onSuccess/onError callbacks are wrapped with
try...catch
now (will try to useerrReporter
in case of exception); - errReporter is wrapped with
try...catch
now (in case of exception raised byerrReporter
, will try to useconsole.error
, if exist).
- User
errReporter
to report errors, and don't useconsole
by default. Comparator
class (andcomparatorFields
parameter) now understand composite fields.- Update NgRx dependencies to v15 (stable).
- Restore global configuration feature (
COLLECTION_SERVICE_OPTIONS
token)
- Make constructors empty (for some reason Angular complains that constructors with arguments are not compatible with dependency injection, although it only happens if code is published to npm).
- make
setOptions()
public.
- Optimize speed of duplicate detection in
read()
- Add
"emitDecoratorMetadata": true
to tsconfig
- New (additional) methods:
createMany()
refreshMany()
updateMany()
deleteMany()
request
parameter will now accept an array of requests (toforkJoin()
them) in:createMany()
readMany()
refreshMany()
updateMany()
deleteMany()
Requests with suffix*many
will run in parallel (usingforkJoin()
). If you need to run them differently, you can use regular methods with the creation operator of your choice.
- A function can be used as a value for the
comparator
field in configuration options or as an argument forsetComparator()
method; - New method for override:
postInit()
- will be called in the next microtask afterconstructor()
.
You can override and use it to call the methods declared in the subclass and still don't bother aboutconstructor()
overriding; getTrackByFieldFn()
helper will not use fields with empty values;- Jest has been configured to run tests in this repo - pull requests are welcome!
readMany()
should updatetotalCountFetched
if provided
onDuplicateErrCallbackParam
added to configuration options.- type restriction for
item
parameter is relaxed to Partial. It was possible to use partial objects before, but it was not obvious from signatures. - New methods
readOne()
,readMany()
,getItem()
,getItemByField()
have been added and documented. - Removed synchronous check from
setUniqueStatus()
whenactive = false
. It was the only synchronous call in code.
- Update dependencies to Angular 15 release version
- Add file with test to the repo
- Add
deleteItemStatus()
method
You can (optionally) declare Collection Service configuration details in your module or component providers:
providers: [
{
provide: 'COLLECTION_SERVICE_OPTIONS',
useValue: {
allowFetchedDuplicates: environment.production,
}
},
]
Token COLLECTION_SERVICE_OPTIONS
is just a string to don't break lazy-loading (if you are using it).
Options structure:
interface CollectionServiceOptions {
comparatorFields?: string[];
throwOnDuplicates?: string;
allowFetchedDuplicates?: boolean; // if not set: true
}
- Fix itemViewModel.isProcessing;
- Make all selectors in
itemViewModel
protected from streams without pre-emitted value; - Add
getTrackByFieldFn()
helper function.
read()
now accepts usual arrays also;- Info about duplicates prevention has been added to README.