README
Angular Redux Data
This project is intended to provide a complete data layer framework for Angular 7+ applications. Angular Redux Data (ARD) utilizes @ngrx and relative libraries to provide redux state management without the developer needing to create excessive and redundant boilerplate code for a standard data schema. The framework also provides a standard DataLayerService to be used with HTTP RESTful apis.
NOTE developers using this library will benefit from having a fundamental understanding of REDUX and @ngrx.
Project Status
Initialized -> Under Construction -> Alpha -> Beta -> Production
Getting Started
The following setup steps are to configure an Angular 7+ application.
Installation
In order to use the ARD framework your angular application must be versioned ^7.1.0
.
Simply install the framework as:
npm install --save angular-redux-data
The installation explicitly depends on the following @ngrx libraries so there is no need to add any dependencies from @ngrx librarys to your project:
{
"@ngrx/effects": "7.0.0",
"@ngrx/entity": "7.0.0",
"@ngrx/router-store": "7.0.0",
"@ngrx/store": "7.0.0",
"@ngrx/store-devtools": "7.0.0"
}
Setup
ARD aims to minimize if not eliminate all of the code overhead that typically comes with Redux based state manage and creating extensive data layer services in Angular application. Large applications can have tens to hundreds of entities in their schema which can make Redux and Angular data layer code extensive and very difficult to maintain/test. ARD needs only a few configuration steps to provide you with UI layer access to all your schema's data.
Configuration
In your project, create a configuration object with the following properties:
export const angularReduxDataConfig = {
effects: [],
entityNameSpaces: [],
defaultHost: '',
defaultPath: '',
entityAdapterMappings: {},
customReducers: {}
};
Config Properties:
- Effects (Required):
This is a list of the ngrx effects services that you will define to handle the standard side effects generated by ARD. There must be an effect service for every entityNamespace provided in order to execute api communication. Effect definition is covered in the following section "Entity Effects"
Example:
import {CommentEffects} from '../effects/comment.effects';
import {PostEffects} from '../effects/post.effects';
effects: [
CommentEffects,
PostEffects
]
EntityNameSpaces (Required):
This is a list of strings for every model in your applications schema. The string provided should be singular or plural based on
the endpoint of the api you will be connecting to for that model. If the path to access post data in your api is /api/posts
then
you will define the entity namespace as 'posts'
. Where if the endpoint to access a user's profile is /api/profile
then
you will define the namespace as 'profile'
Example:
entityNameSpaces: [
'posts',
'comments',
'profile',
'breeds',
'events'
]
defaultHost (Required): This is the host location that the data layer will default to given no custom entity adapter is provided (see entityAdapterMappings).
*The default host should not end or begin with a back slash
example
defaultHost: 'https://jsonplaceholder.typicode.com'
defaultPath (Required): This is the default base path the the data layer will append to the default host given no custom entity adapter configuration. The host and path concatenated form the base of the endpoint url.
*The default path should not end or begin with a back slash
defaultPath: 'api/v1'
entityAdapterMappings
It is possible to have a different api for one or many of the entities in the schema of your application. This feature enables an
application to use ARD no matter how the api(s) for the various models are structured. The first step in defining a custom adapter is
to create an adapter class that extends the ARD DataAdapter
class if you have the need to customize how you interface with a particular API.
If you are simply defining a separate endpoint but the REST patterns are standard then simply extend the ApplicationHttpAdapter
class.
example adapter with standard REST API patterns (no need to override methods):
import { ApplicationHttpAdapter } from 'angular-redux-data';
import { Store } from '@ngrx/store';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/';
export class CatsAdapter extends ApplicationHttpAdapter {
constructor(protected _http: HttpClient,
protected _host: string,
protected _path: string,
protected _store: Store<any>) {
super(_http, _host, _path, _store);
}
}
example adapter with non-standard REST API patterns (need to override methods):
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Store} from '@ngrx/store';
import {DataAdapter} from 'angular-redux-data';
import {Observable, throwError} from 'rxjs';
import {catchError, distinctUntilChanged, filter, map} from 'rxjs/operators';
export class TicketMasterAdapter extends DataAdapter {
private apiKey: string;
constructor(protected http: HttpClient,
protected host: string,
protected path: string,
protected store: Store<any>) {
super(http, host, path, store);
// Example of how to utilize an auth token in the adapter class
}
// Implemented Method
createRecord(type: string, data: any): Observable<any> {
// API interface code
}
// Implemented Method
deleteRecord(type: string, recordId: number | string): Observable<any> {
// API interface code
}
// Implemented Method
findAll(type: string, config?: {}): Observable<any[]> {
// API interface code
}
// Implemented Method
findRecord(type: string, recordId: number | string, config?: {}): Observable<any> {
// API interface code
}
queryAll(type: string, params: {}): Observable<any> {
// API interface code
}
// Implemented Method
updateRecord(type: string, recordId: number | string, data): Observable<any> {
// API interface code
}
}
After your custom adapters have been defined you can then define the entityAdapterMappings
object in your ARD configuration. Using the two
examples above the object would look like:
entityAdapterMappings: {
'breeds': {
adapter: CatsAdapter,
host: 'https://api.thecatapi.com',
path: 'v1'
},
'events': {
adapter: TicketMasterAdapter,
host: 'https://app.ticketmaster.com',
path: 'discovery/v1'
}
},
customReducers: {
'uiState': uiState
}
}
customReducers:
ARD enables you to easily establish REDUX state patterns outside the data layer where the flows are specific to the application or component(s) state.
This is done in a similar manner to how you would pass reducers references in the NgRx StoreModule
. @ngrx
example:
import {uiState} from '../app/<path to reducer>/uiStateReducer';
customReducers: {
'uiState': uiState
}
FULL EXAMPLE:
export const reduxDataServiceConfig: {
effects: [ // These references are explained in the following section: entity effects
CommentEffects,
PostEffects,
ProfileEffects,
BreedEffects,
EventEffects
],
entityNameSpaces: [
'posts',
'comments',
'profile',
'breeds',
'events'
],
defaultHost: 'https://jsonplaceholder.typicode.com',
defaultPath: '',
entityAdapterMappings: {
'breeds': {
adapter: CatsAdapter,
host: 'https://api.thecatapi.com',
path: 'v1'
},
'events': {
adapter: TicketMasterAdapter,
host: 'https://app.ticketmaster.com',
path: 'discovery/v1'
}
},
customReducers: {
'uiState': uiState
}
}
app.module
After your configuration is completed all you have to do is import the following modules into your app.module.ts
and pass the configuration
object in as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {reduxDataServiceConfig} from '../<path to configuration object>'
import { AngularReduxDataLayerModule, AngularReduxDataModule, ReduxDataReducerFactory } from 'angular-redux-data';
import { StoreDevtoolsModule, StoreDevtoolsOptions } from '@ngrx/store-devtools';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
// Begin ARD specific modules
AngularReduxDataModule.forRoot(reduxDataServiceConfig),
AngularReduxDataLayerModule.forRoot(reduxDataServiceConfig),
StoreModule.forRoot(ReduxDataReducerFactory.getReducers(
reduxDataServiceConfig)
),
EffectsModule.forRoot(reduxDataServiceConfig.effects),
StoreDevtoolsModule.instrument(<StoreDevtoolsOptions>{maxAge: 25}),
// End ARD specific modules
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
Entity Effects
In order retrieve data from your api endpoint(s) you will need to setup an Effects Service @ngrx/effects for every model in your schema.
ARD streamlines the data layer interaction for ngrx/effects in Angular application via the DataLayerService
that is established via the AngularReduxDataLayerModule
imported in your app.module.ts
.
In each Effects service that you want to utilize the data layer adaptor simply extend the AngularReduxDataEffect and pass the namespace into
the super()
constructor argument.
import {Injectable} from '@angular/core';
import {Actions, Effect} from '@ngrx/effects';
import {AngularReduxDataEffect} from 'angular-redux-data';
import {ReduxDataActionsService} from 'angular-redux-data';
import {DataLayerService} from 'angular-redux-data';
@Injectable()
export class BreedEffects extends AngularReduxDataEffect {
@Effect() findAll$;
@Effect() findRecord$;
@Effect() create$;
@Effect() delete$;
@Effect() update$;
@Effect() queryAll$;
constructor(protected actions$: Actions, protected actionsService: ReduxDataActionsService,
protected dataLayerService: DataLayerService) {
super(actions$, actionsService, dataLayerService, 'breeds');
}
}
The definition of these files will be what are referenced in your AngularReduxData configuration object.
Overriding ARD Effects
There may be instances where you want to have a completely different side effect for an effect method in a particular entity. This is easily done by overriding the respective method in the constructor of the Effect Service class.
import {Injectable} from '@angular/core';
import {Actions, Effect} from '@ngrx/effects';
import {AngularReduxDataEffect} from 'angular-redux-data';
import {ReduxDataActionsService} from 'angular-redux-data';
import {DataLayerService} from 'angular-redux-data';
@Injectable()
export class BreedEffects extends AngularReduxDataEffect {
@Effect() findAll$;
@Effect() findRecord$;
@Effect() create$;
@Effect() delete$;
@Effect() update$;
@Effect() queryAll$;
constructor(protected actions$: Actions, protected actionsService: ReduxDataActionsService,
protected dataLayerService: DataLayerService) {
super(actions$, actionsService, dataLayerService, 'breeds');
// Override the default action of the findRecord effect
this.findRecord$ = actions$
.pipe(
ofType(this.actionsService.actions[this.entityNamespace].actionStrings.FIND_RECORD),
map(() => {
// ...new actions performed here
return new ofType Action<any>; // must return an object that implements {Action} from '@ngrx/store';
}));
}
}
Use
Using Angular Redux Data to read and manipulate data from your api(s) is exceedingly simple. There are several basic functions
that should enable almost every application to accomplish its data interfacing goals. Simply inject the AngularReduxDataService
into
any component that needs to access data.
constructor(private _ard: AngularReduxDataService) {}
On a successful action, each of the methods in the service will return an Observable of the relative selection from the Redux store.
The methods for reading data:
findAll(nameSpace: string)
findRecord(nameSpace: string, recordId: string | number)
queryAll(nameSpace: string, params: {})
peekAll(nameSpace: string)
peekRecord(nameSpace: string, recordId: string | number)
peekBy(nameSpace: string, params: {})
will emit every time the Redux store data of the entities relative to the action are updated.
The methods for writing data:
create(nameSpace: string, data: any)
update(nameSpace: string, recordId: string | number, data: any)
delete(nameSpace: string, recordId: string | number)
will emit once upon the completion of the given action.
Errors
If the action executed fails to read or write data against the API endpoint an error will be thrown returning the relative Transaction object for the action containing the following properties.
export class ArdTransaction {
id: string; // Unique identifier for every transaction
type: TransactionType; // see TransactionType
entityNamespace: string;
createdAt: number;
updatedAt: number;
success: boolean; // true if api request returns success
failed: boolean; // true if api request returns error
error: any; // error returned via failed network request
entities: string[] | number[]; // list of ids in Redux store related to the transaction
}
enum TransactionType {
'findAll' = 'Find All',
'findRecord' = 'Find Record',
'queryAll' = 'Query All',
'create' = 'Create',
'update' = 'Update',
'delete' = 'Delete'
}
Actively Reading Data
Actively reading data refers to the flow of making a request to the relative api endpoint, updating the Redux store with the data returned, and then observing the relative selection from the Redux store. There are three functions that enable active reading of data:
Find All
This method will make a request to the relative api endpoint for all the entities in a collection. When the request completes and the data is updated in the Redux store an observable of that entity collection in the store will be returned. The observable returned will emit every time a change occurs in that collection in the store until the subscription is completed.
findAll(namespace: string)
Example:
this._ard.findAll('posts').subscribe(posts$ => {
this.posts = posts$;
}, (err) => {
console.log('error communicating with api', err)
})
*From our configurations above this will make a request to
GET: https://jsonplaceholder.typicode.com/posts
Find Record
This method will make a request to the relative api endpoint for a single entity in a collection. When the request completes and the data is updated in the Redux store an observable of that entity from the collection will be returned. The observable returned will emit every time a change occurs in that object in the store until the subscription is completed.
findRecord(namespace: string, recordId: string | number)
this._ard.findRecord('posts', 1).subscribe(postWithIdEqualTo1$ => {
this.post = postWithIdEqualTo1$;
}, (err) => {
console.log('error communicating with api', err)
})
*From our configurations above this will make a request to
GET: https://jsonplaceholder.typicode.com/posts/1
Query All
This method will make a request to the relative api endpoint with filter parameters in order to return a subset of a collection. When the request completes and the data is updated in the Redux store an observable of that entity from the collection will be returned. The observable returned will emit every time a change occurs in that selection of entities in the store until the subscription is completed.
queryAll(nameSpace: string, filterParameters: {}) : Observable<any>
this._ard.queryAll('comments', {postId: this.post.id}).subscribe(comments$ => {
this.post['comments'] = comments$;
}, (err) => {
console.log('error communicating with api', err)
});
*From our configurations above this will make a request to
GET: https://jsonplaceholder.typicode.com/comments?postId=1
Passively Reading Data
To reduce the amount of network requests being sent from an application, data from the Redux store can be subscribed to in the three following ways:
Peek All
If you wish to observe the entire collection and any changes that occur in that collection.
peekAll(namespace: string)
this._ard.peekAll('posts').subscribe(posts$ => this.postList = posts$);
Peek Record
If you wish to observe a single object in a collection and any changes that occur to that entity.
peekRecord(namespace: string, recordId: string | number)
this._ard.peekRecord('posts', 1).subscribe(post$ => this.post = post$);
Peek By
If you wish to observe a filtered selection of entities from a collection.
peekBy(nameSpace: string, filterParameters: {}) : Observable<any>
this._ard.peekBy('comments', {postId: this.post.id}).subscribe(comments$ => this.post.comments = comments$);
Writing Data
Manipulating data is a simple and straightforward as reading data. Each of the following functions will map the correct action, adapter, etc. via the namespace string you provide in the first parameter of the method After a successful write the Redux store will be updated with the new state of the entity and will emit once after the flow is complete. If there is an error returned by the api endpoint an observable error will be thrown with the relevant transaction object containing all of the error status information from the api and the application.
Create
To create a new entity within a collection simply provide the namespace of the collection and a POJO of data for the new entity.
create(nameSpace: string, data: {}) : Observable<any>
this._ard.create('posts', {
'title': 'testing post',
'author': 'meow'
}).subscribe((post$: any) => {
// Will only emit once when creation flow is completed
this.newlyCreatedPost = post$;
}, (err) => {
console.log('error communicating with api', err)
});
Update
Updating existing entities is similar to creating entities however you must pass the id of the object in addition to an object of the fields that you wish to update.
update(nameSpace: string, recordId: string | number, data: {}) : Observable<any>
this._ard.update('posts', 1, {meow: 'mix'})
.subscribe((updatedPost$: any) => {
// Will only emit once when creation flow is completed
}, (err) => {
console.log('error communicating with api', err)
});
Delete
When deleting an object pass the namespace and the id of the entity you wish to delete.
Upon a successful response the observable value returned will be a boolean of true
. The
entity will no longer exist in the Redux store.
*If the api returns an error status the entity will still exist in the Redux store.
delete(nameSpace: string, recordId: string | number): Observable<any>
this._ard.delete('posts', this.newlyCreatedPost.id)
.subscribe((deleted$: boolean) => {
// Will only emit once when creation flow is completed
}, (err) => {
console.log('error communicating with api', err)
});
Support:
For bugs reports, general usage questions, and feature requests please create an issue in github.
For application specific assistance and consultation please send inquires to: noskiboots@gmail.com
Please include the following information in the request:
- Name
- Company
- Version of Angular
- Brief description of project and needs