Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Own your app and do what you need, when you need
While we strive to provide as much commonly implemented functionality as we can, there can always be cases where you, the developer, must take control. Since this library is implemented much like any standard @ngrx application, it allows you to inject your own behavior whenever you need to.
If you require custom behavior for anything, you are always free to implement your own custom actions & custom effects. You are also free to hook into our library and framework by dispatching our actions from your own actions.
This could, for example, allow you to implement a custom search behavior with your own SearchEntityWhatever
action and searchEntityWhatever$
effect, and dispatch the Auto-Entity relevant success and failure actions on response, which will be handled by our meta reducer. This would integrate a custom initiating action and data from a custom service call into Auto-Entity managed state.
It is possible to integrate custom actions and effects in a variety of ways, allowing you to leverage the lower level power of NgRx when necessary, while still leveraging the ease of use offered by NgRx Auto-Entity.
Understand changes to model implementation
With NgRX Auto-Entity, a very small change must be made to how models are implemented. Standard @ngrx models are implemented as interfaces. A normal model used with @ngrx/entity may look like this:
This simple model would then be wired into @ngrx/entity using the adapter, with a selectId handler to allow entity framework to determine what the primary key of the entity is.
Due to the dynamic nature of how NgRX Auto-Entity works, interfaces are insufficient as they are compile-time only types in TypeScript. All of their information is ultimately stripped from the transpiled and optimized JavaScript code that you will ultimately release to a web server. To combat this issue, we must convert our entities to classes, which are a native runtime feature of JavaScript.
In addition to converting our model from an interface to a class, notice that we have also imported Key
from @briebug/ngrx-auto-entity
and decorated our id
property with @Key
. This is how the Auto-Entity framework is able to determine which property of the model represents the primary key. As with @ngrx/entity, this key is used to organize and look up entities stored in state.
Note that Auto-Entity supports composite keys. Simply decorate each property that participates in a primary key with the @Key
decorator if necessary.
A simple but common example of a composite key might be something along the lines of an order line item, which references both an order and a product along with a quantity:
For more detail about how composite keys work, read the advanced usage documentation on models and keys.
Whats mine is yours, whats yours is mine!
When the need arises to create a custom action & custom effects, Auto-Entity can still help you reduce your workload. It may be that you can integrate your actions, effects and data with Auto-Entity managed state, so long as the data is compatible.
As a simple example, you may wish to create a custom action for handling type-ahead lookups that search a given entity.
You could then implement your own effect for handling this action to ensure the proper implementation and behavior:
In this case, we need switchMap
semantics...that is, we wish to cancel any previous requests that may have stale information in order to utilize the most up to date search criteria the user may have entered.
By default, Auto-Entity uses mergeMap
for all effects, which may not always be the most appropriate mapper for the use case.
Take note, however, of the new actions returned by this effect:
We are returning generic actions! Since these are customers
, as long as we have built auto-entity state for the Customer
entity, you may combine custom actions you implement yourself with generic actions from the Auto-Entity library.
These actions will ultimately cause the returned customer search results to be reduced into state managed by Auto-Entity for you. You only have to create just one action. Just make sure the shape of the data returned by your custom service is compatible, or is otherwise transformed into a compatible shape.
Trifurcation Avoidance Paradigm
The primary interface for an application to interact with state in @ngrx is actions. Actions encapsulate intent, along with the information necessary to perform the requested intent. Actions are one of the key sources of "boilerplate" code with standard @ngrx, and one of the primary areas we aim to simplify with Auto-Entity.
With @ngrx, defining actions tends to require defining action types and action classes "in triplicate", as most actions, particularly entity actions, usually require an "initiator" paired with "result" actions. Initiators are dispatched to request that "some action be performed", while results are dispatched in order to denote that "a requested action completed" with success or failure. Actions, therefor, tend to trifurcate
in their actual implementation.
Understand the changes to defining service providers
For most Angular applications, services can be provided very simply, simply by including the class in the array of providers
in your module. With NgRX Auto-Entity, due to its dynamic nature we must change how services for entities are provided a little bit.
Instead of registering the service itself directly, we must instead register the model class as the provider, and map it to an entity service via the useClass
option. Like so:
When dispatching an action, the model class is specified as the first parameter, and as such the model class is the only thing NgRX Auto-Entity can use to look up the necessary service provider. By mapping the model class to the service class, you are leveraging a standard Angular paradigm to support dynamic service lookup at runtime.
Learn about the required paradigm changes for Auto-Entity
For NgRX Auto-Entity to function properly, some minor changes will be required in the way you implement a couple standard bits of code. This includes service implementations and model implementations. These changes are not particularly significant, and for services we provide a lot of meta information to assist you in achieving whatever is necessary for your applications.
As models and services are standard elements of any Angular application, there should be no additional boilerplate code to implement here. These would be implemented regardless. However, be aware of the nuanced changes in how the model and service are used, and the explicit interface requirement for the service.
More significant changes occur when utilizing facades to interact with state. Facades encapsulate the complexities of @ngrx, presenting you with a simplified, logical and more standard API into working with your stateful data.
Understand changes to service implementation
With a normal @ngrx application, you are free to implement services however you see fit. There are no explicit requirements by @ngrx, since you are also on the hook to implement the effects as well, and it is your own effects that call your services.
NgRX Auto-Entities dynamic nature requires that data services implement the IAutoEntityService<TModel>
interface. This interface defines the contract by which Auto-Entity interacts with your services. This interface is described as follows:
This interface defines a different method that corresponds to each different kind of initiating generic action provided as a part of the NgRX Auto-Entity library. Every method provides entityInfo
as the first parameter, which contains metadata about the entity, including its name (based on the class name you defined for the model) as well as its type (the class you defined for the model). Each method also accepts a variety of other parameters, whatever may be necessary to support the unique behavior of that method.
Note that each method of the IAutoEntityService
interface is optional. Implement only what you need!
In addition to the basic CUD operations, we also support bulk versions of each CUD operation, as well as several options for loading entities. Each of the different load operations support different behavioral semantics, defined by the initiating actions and guiding how the meta reducer handles the data in state.
Each method of the IAutoEntityService
interface accepts, as the last parameter, custom criteria
. This allows you, the developer, to include any additional, arbitrary details that may be necessary to facilitate the desired operation, for any operation supported by this library.
Initialization Simplified
At the core of NgRx Auto-Entity is the buildState
function. This utility function encapsulates all the "boilerplate" that you would normally implement yourself when building up state for a new entity. Where in the past you may have manually created an @ngrx/entity adapter, built out your standardized reducer function, and exported a bunch of support elements like initial state, selectors, a key selection handler, etc.
Now you simply need to call buildState()
like so:
That is about as simple as it gets, unless you need to do anything custom, such as add your own additional support actions, use selectors, or retrieve the prefabricated facade base class so you can keep your state encapsulated.
The buildState
function provides all of the functionality necessary to take control when you need to, without being complex, and all while still handling most of the work for you. The full expression of a buildState
call would be as follows:
In addition to providing the initial state version for your stub reducer, the buildState()
function also provides a selectors
map, an entityState
selector to support the creation of custom selectors, a base facade
class (source of ultimate power and simplicity!), and finally a ready-made stub reducer
(only works with non-AOT builds, sorry!)
Core to NgRx Auto-Entity is it's internal state structure for each entity. You may have noticed the little IEntityState<TModel>
interface floating around prior documentation, including the Quick Start. This interface is much like the EntityState<T>
interface from @ngrx/entity, although more complex as Auto-Entity manages more state for you.
While our internal types are named slightly differently, the structure of our IEntityState
interface is name-compatible with the structure @ngrx/entity expects. As such, with perhaps a little type coercion when necessary, it should be possible to utilize @ngrx/entity functionality such as its adapter to update Auto-Entity state...if the need ever arose.
You may notice that we track a lot of additional but optional state as well. This includes the current entity, information about the current page or range loaded into state, as well as various flags and timestamps.
This interface is fundamental to all entities who's state is managed by NgRx Auto-Entity. You can and should use this interface wherever a state interface is required, such as root or feature state interfaces that will be used in a reducer map.
Learn this interface if you intend to leverage any of the lower level capabilities of NgRx Auto-Entity. In particular, if you ever provide extra initial state, make sure you know what properties are involved in the event you wish to ADD new state information, or SET existing state information with initial values.
One,All, Many, Pages, Ranges...
The generic actions in the NgRx Auto-Entity library cover all the CRUD bases, as well as a range of loading options, plus some extra useful "utility" actions to cover other common state-related entity tasks. First up, we provide several options for loading data:
Load: one entity at a time
LoadAll: every entity all at once
LoadMany: lots of entities in arbitrary bunches
LoadPage: entities in discrete bunches
LoadRange: entities in sequential bunches
Each load action is imbued with unique semantics and behavioral implications. For example, dispatching a LoadAll
action implies that you wish to replace any previously existing entity state for the given model with whatever set of entities is retrieved by the entity service for that model.
Dispatching LoadMany
on the other hand implies that you wish to keep any previously existing entity state for the given model, and merge in whatever set of entities is retrieved. Similarly, Load
will also merge the retrieved entity into any previously existing state.
Further, pages are semantically different than ranges. A page is a discrete slice of a set of entities with a distinct beginning and end. A range on the other hand is a sequential slice of a set of entities that ultimately form a continuous range. When dispatching LoadPage
the implication is that you wish to replace any previously existing state for the given model.
The explicit use case here is when the entire set of all entities for a given model is simply too large to fit in memory in a browser (i.e. you may have tens of thousands...millions...billions of records in a database.) Standard case for tables with paging controls.
When dispatching LoadRange
on the other hand, the implication is that you wish to join newly loaded ranges of entities onto existing state for the given model. The primary use case here is infinite scrolling, where additional entities are added to previously loaded entities in response to continued vertical scrolling or horizontal panning by a user.
Much like @ngrx/entity, when you build state for a new entity with NgRx Auto-Entity we provide a set of ready-made common selectors. These selectors allow you to retrieve the state Auto-Entity manages for you.
We have attempted to maintain compatibility with @ngrx/entity here, so we include the core standard set of selectors that are provided by that framework.
To name a few...
selectAll
selectEntities
selectIds
selectTotal
...
These selectors are returned from our buildState
utility function as a ISelectorMap<TParentState, TModel>
. See the reference documentation for the full interface definition and the complete list of selectors.
Create, Update, REPLACE & Delete
First off, you may be wondering why I am trying to coin a new term here. CURD? "Ain't it supposed to be CRUD?!" you say? Just wait till you hear the full "new" acronym! CURDL! Ah, the cow jokes that could be made... Well, CURDL, because: Create, Update, Replace, Delete & Load! These are the core actions supported by Auto-Entity, and they span the range of common entity-related behaviors.
CRUD in the past has stood for Create, Read, Update, Delete. This was a fairly standard term, however one of the goals of NgRx Auto-Entity is to fully support the entire range of HTTP/REST methods currently standardized. This includes both PUT and PATCH, which now have the semantics of "Replace in Whole" vs. "Update in Part." Auto-Entity provides actions to support both methods concurrently, required.
Create: Create a single entity [POST]
CreateMany: Create multiple entities (batch) [POST]
Update: Update a single entity [PATCH]
UpdateMany: Update multiple entities (batch) [PATCH]
Replace: Replace a single entity [PUT]
ReplaceMany: Replace many entities (batch) [PUT]
Delete: Delete a single entity [DELETE]
DeleteMany: Delete many entities (batch) [DELETE]
Aside from supporting a full set of CURD actions for singular entities, we also support batch/bulk CURD actions allowing the creation, update/replacement and deletion of multiple entities at once.
Going the extra mile
In addition to the "common" selectors that match @ngrx/entity selectors from the adaptor, NgRx Auto-Entity also provides selectors for accessing all of the extra state it also automatically tracks and manages for you. This includes loading/saving/deleting timestamps as well as flags, currently selected entities, etc.
selectCurrentEntity
selectCurrentEntities
selectCurrentEntityKey
selectCurrentEntitiesKeys
selectCurrentPage
selectCurrentRange
selectTotalPageable
selectIsLoading
selectIsSaving
selectIsDeleting
selectLoadedAt
selectSavedAt
selectDeletedAt
As with common selectors, these are returned from our buildState
utility function as part of the ISelectorMap<TParentState, TModel>
. See the documentation on buildState
for the full interface definition. You export these selectors the same as any other, via destructuring the selectors
object returned by buildState
.
It should be noted that all state managed by Auto-Entity is "sparse" or lightly populated. This means that some state, say the current page or range, the total pageable count, even the current entity, may be null or undefined until such time as an action is dispatched that would result in that state becoming populated. As such, expect that depending on the actual state of the application, null
or undefined
may indeed be the result of using any of the above selectors.
While NgRx Auto-Entity can do a lot for you, there may be times when you need to perform more complex operations than are supported with the ready-made generic actions of Auto-Entity. In such situations, you can always fall back onto "classic" NgRx and leverage all the raw power it provides.
There are a few scenarios where you may need to fall back onto regular old NgRx. Purely custom functionality that doesn't fall into the "entity" mold may simply require a "full on NgRx." The top use case that comes to mind would be your average user login scenario, which often requires actions and server interactions that fall into more of a challenge/response sort of paradigm than an entity CRUD paradigm.
In the auth case, aside from just Load(User, id)
the actual act of Authentication will usually be a custom action, custom effect, probably a custom service method (although it is fine to add additional functionality to entity services if you so require! Entity services may be provided directly as well as via the model if necessary.)
There are a couple other scenarios where you may need to implement some custom functionality, however you may also still be able to leverage Auto-Entity to one degree or another. Such cases may include say a bundling complex behaviors such as a multi-entity save into a single action, and leveraging a custom effect to ultimately dispatch the necessary generic actions to work on each individual entity behind the scenes.
Another case may be when you need a workflow, where one action begets another action, and so on. In this case custom effects may be leveraged to watch for particular generic actions dispatched for particular entities, and in turn dispatch other generic or custom actions for other entities or behaviors. This is where the power of reactive programming really comes into play.
Clearing, Selecting & Deselecting, Oh my!
Beyond the core CURDL actions, Auto-Entity also provides a few utility actions that cover other common entity-state related functionality. This includes an action to clear the state for a given model, as well as select and deselect a particular entity for a given model by reference or key.
Recently added in v0.2 is the ability to select and deselect a set of entities for a given model by references or keys.
Clear: Empties the state for a given model
Select: Select the given single entity
SelectByKey: Select an entity who's key matches the specified key
SelectMany: Select the specified entities as a set *
SelectManyByKeys: Select entities who's keys match the specified keys as a set *
Deselect: Deselect any previously selected single entity
DeselectMany: Deselect the specified entities from current entity set
DeselectManyByKeys: Deselect entities who's keys match the specified keys
DeselectAll: Deselect all entities in current entity set
For selecting entities by key, if an entity uses a composite key we provide some useful utility functions for retrieving an entities key. For more information about how composite keys are generated in Auto-Entity, read the advanced documentation on models and keys.
When selecting, it should be understood that single entity selections and multiple entities selected as a set are tracked separately. It is possible to select a single entity, as well as select a set of entities, concurrently. Deselecting a single entity does not deselect the set, nor does deselecting a set deselect the single entity.
This in turn allows a simple hierarchical selection capability, wherein a subset of entities may be selected, then a single entity within the subset could be highlighted. It is also possible for the two selections to be entirely disjoint and unrelated.
In addition to our "standard" loading actions for single, all and many entity retrievals, we also provide two additional loading actions: LoadPage
and LoadRange
. You may wonder why both, and again it boils down to semantics, behavior. Both are necessary to support different kinds of data loading behavior, as will become evident here.
Loading a "range" of data is similar in concept to loading a page of data, however it differs in semantics and actual behavior in the context of NgRx Auto-Entity and the way reduction of page loads are handled vs. the way reduction of ranged loads are handled.
The LoadRange
generic action supports a more modern approach to handling very large, and more specifically arbitrarily sized datasets called "infinite scrolling." This technique is usually implemented on lists of items, such as products in a catalog, that may have relatively arbitrary and often-changing total entity counts. When a user scrolls to the bottom of the browser page, the next "range" of products, starting from the last+1 of the currently displayed range, is automatically retrieved from the server and displayed as a continuation of any previously displayed list.
When loading a range, unlike loading a page, any newly loaded entities for this type will be merged into existing state, preserving any previously loaded entities and including any newly loaded entities. As with paged loads, this ensures that bandwidth is used efficiently, however keep in mind that for very large data sets, continuous loading of subsequent ranges could eventually require significant amounts of browser memory to store all the information in state. Further, due to the pure
(side-effect free) nature by which new versions of state must be created with @ngrx, updating state with a large volume of previously loaded entities may start to become a lengthy operation.
Loading ranges of data one at a time with NgRx Auto-Entity is also very easy. Simply use the LoadRange
action, or the loadRange
method on your entity facades, along with the Range
parameter:
When implementing your entity service, you will have access to the Range
object provided in the initial action, which may be leveraged in your API call:
Note the use of pipe()
on the http
call here, and the transformation of the response. Also not the return type of the loadRange
method. NgRx Auto-Entity requires additional meta-data about what range was loaded as well as the total number of entities that may be ranged through, in order to store that information in state and make it available to components and your UI (for proper handling of infinite scrolling behavior, for example).
It is important to understand the nature of the data that must be returned by the loadRange
method in your entity services. The return type is IEntityWithRangeInfo<TModel>
which is defined as so:
The child type of the property rangeInfo
, of type IRangeInfo
is defined as so:
Unlike with paged data, where a total count is essential in order to properly calculate how many pages of data there are, ranged data does not necessarily require an exact total count. For use cases where a total count is not required, you may simply specify Infinity
or any other constant that allows you to implement your ranged loads and UI behavior properly.
In addition to our "standard" loading actions for single, all and many entity retrievals, we also provide two additional loading actions: LoadPage
and LoadRange
. You may wonder why both, and again it boils down to semantics, behavior. Both are necessary to support different kinds of data loading behavior, as will become evident here.
Loading of paged data is a common practice that dates back many years, in fact decades. It is a fundamental kind of data load, and extensive research has been done as well as dedicated features to support paged data have been integrated into data storage solutions.
Loading pages of data, rather than loading complete sets of data, is often essential. This is especially true in the modern world where we frequently must acquire and work with immense datasets that can number in the millions, billions...even trillions of records. With such numbers, it should be obvious that loading all entities for such datasets would be problematic at best, assuming it was even possible in the first place. Large datasets will usually consume immense space, including immense bandwidth and immense amounts of memory in a browser.
When loading a page, any previously loaded entities for this type will be replaced in by the new page of entities retrieved. This ensures that bandwidth and browser memory space is used efficiently, and that memory is not wasted trying to store an ever-growing number of entities as a user cycles through pages in your UI.
Loading pages of data one at a time with NgRx Auto-Entity is quite easy. Simply use the LoadPage
action, or the loadPage
method on your entity facades, along with the Page
parameter:
When implementing your entity service, you will have access to the Page
object provided in the initial action, which may be leveraged in your API call:
Note the use of pipe()
on the http
call here, and the transformation of the response. Also not the return type of the loadPage
method. NgRx Auto-Entity requires additional meta-data about which page was loaded as well as the total number of entities that may be paged through, in order to store that information in state and make it available to components and your UI (for proper rendering of paging controls, for example).
It is important to understand the nature of the data that must be returned by the loadPage
method in your entity services. The return type is IEntityWithPageInfo<TModel>
which is defined as so:
The child type of the property pageInfo
, of type IPageInfo
is defined as so:
One way or another, you must be able to provide the above information for each paged load. You may simply prefer to reflect back the original page information sent in for the IPageInfo
page property, however usually the totalCount
must be returned by the server in one way or another. It need not necessarily be on the same call, an additional call may be performed to retrieve the total count of entities:
Another possibility, although potentially not canonical standard depending on exactly how you implement it, might be to use the HTTP Content-Range
header in your responses to support additional metadata about page info, without having to include it in the body of the response. This header is normally used with a unit of bytes, however technically speaking the units can be arbitrary (such as, say, records
or entities
):
Current Complexity with Action Definitions
A standard approach to implementing actions with @ngrx requires defining an enumeration of action types, which map a code identifier to a string, implementation of an action class that derives from Action
and the concatenation of each action type into an action union that allows proper implementation of a reducer function to reduce actions and the information they contain into your state.
This is a lot of work required just to add the ability to create an entity. Not only do you need the ability to handle the initial create request, but also deal with the success or failure of that request, at the very least. So each "action" generally tends to trifurcate, and thus the action types and action union code trifurcates as well.
Note: The most recent release of NgRx, version 8, has introduced some utility functions that can help reduce some of the "boilerplate" nature of implementing actions, effects and reducers. These new functions are a welcome improvement over prior versions of NgRx...however, we do believe they still fall short of providing the kind of simplified, rapid development experience Auto-Entity provides.
Once actions are defined, one may then dispatch them using the @ngrx store. This is usually done within Angular container components:
For actions to actually do anything, however, you need more. Your work does not end here. You still need effects, and reducers, to make any of these dispatched actions actually perform useful work and update state.
When ready-made just isn't enough
While NgRx Auto-Entity provides quite a bit of ready-made functionality, tracking state for many of the most common elements of entity state including the usual "custom" aspects such as the currently selected entity, loading flags, etc. there may be cases where you still need to create a custom selector. Even when using prefabricated facades, once you get into extending those facades, custom selectors are not uncommon.
Custom selectors are implemented as you have always implemented them. Nothing new or special there. Parameterized selectors may also still be used. Make sure to extract the entityState
property from the return value from buildState
if you need to access the whole state (including all of the additional data tracked by Auto-Entity) for a given entity.
Don't forget to destructure the selectors
map to pull out the specific selectors required for composition into custom selectors. Also remember that nested structures may be destructured inline with modern javascript as well!
Note that it is also possible to create parameterized selectors with @ngrx. You may still implement parameterized selectors with Auto-Entity as well:
Follow the necessary precautions and best practices outlined in the ngrx.io documentation when using parameterized selectors. There are some important caveats to be aware of with their use.
Radical Simplification with Reusable Action Libraries
NgRx Auto-Entity provides a relatively simple solution to the action triplet trifurcation conundrum. Make commonly-implemented actions reusable! If there ever was a use case for generics, CRUD entity actions would seem to be as sublimely perfect a case as ever possible. By making actions generic, this allows a simple library of standard CRUD actions to be implemented that could then be easily reused by any number of unique entities.
Generic actions are the primary interface with which an application interacts with NgRx Auto-Entity. No need to implement the same pattern of three actions again and again for each and every entity! We have provided all the necessary actions for you. You simply need to dispatch them!
Aside from no longer having to implement your own actions, everything else should be business as usual. Your controllers, when dispatching generic actions from Auto-Entity directly, should look quite familiar.
Each generic action's constructors require a standard set of parameters, and some actions may require unique parameters to perform the necessary function. Action constructors are as follows: