angular-redux-data

A Redux based data layer framework for Angular Application

Usage no npm install needed!

<script type="module">
  import angularReduxData from 'https://cdn.skypack.dev/angular-redux-data';
</script>

README

Angular Redux Data

Join the chat at https://gitter.im/angular-redux-data/community

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
  • Email
  • Company
  • Version of Angular
  • Brief description of project and needs