Optimal Angular : PubSub With NGRx

Willie Streeter
will.streeter
Published in
15 min readJun 12, 2017

--

The concepts and practices presented in this article are delineated in a previous article, Practical Web Development and Architecture. The development of a front-end application base on those concepts will be explained in the following exposition, which was constructed in concert with a NodeJS server application and a Mongo data store. A review of the server application is offered in the article Swagger, NodeJS, & TypeScript : TSOA. The code can be reviewed in the ws-ngx-login-demo GitHub repository. Both front-end and back-end applications are bundled as a FullStack integrated development environment using Docker, delineated in the article Docker is my {I.D.E} . To run this application with in the FullStack clone the ws-dev-docker-example .

For those not interested in using Docker, but wanting to clone, build, and run the application, I have create branch in the GitHub repository, called serverless , which can standup without the use of Docker, the NodeJS server application, or Mongo.

Itinerary

The following article demonstrates a use of the latest Angular 4.1.0( at the time of this writing) and NGRX ( Redux) implementation to show a registration and login flow, with guarded content.

  • Development Set Up: Summarize the development apparatus tools and configuration
  • App Directory Configuration: Highlight the contents and approach to the structure of the application directories.
  • Data Management and Processing: Detailed explanation of how a data flows and is processed with in the ‘separations of concerns’. The means of providing a User’s profile view will be used to generalized the process.

In the section DataManagement and Processing, a more detailed break down of several areas is covered with regard to PubSubBroker and the use of NGRx:

  • Generating a User Profile View in 7 Steps : specific code snippets showing how the ProfileComponent in the view-layer interact with the PubSubBroker in the business-layer to facilitate communication with the data-layer.
  • Deconstructing PubSubBroker & Brokerage : details the construction of the object oriented approach to crafting PubSubBroker and Brokerage to facilitate decoupling the view-layer from the data-layer.
  • Processing global State data with NGRx : a brief synopsis on how data is processed with the NGRx’s implementation of Redux.
  • Encapsulated Services : is an example of how business-layer services outside of the PubSubBroker, can be shared across the view-layer components as an encapsulated processes. This will be achieved by delineating how Angular’s Reactive Forms are extended with validator.js to customize validations with the view-layer’s RegistrationComponent.

Development Set Up

Scaffolding of the source code for this project is accomplished with Minko Gechev’s angular-seed, which uses jspm’s SystemJS universal module loader. Currently, the latest Angular X uses WebPack through the angular-cli tool. WebPack relies on an accumulation of plugins to handle bundling strategies and task maintenance such as preprocessor css compiling, minifying, and other scaffolding nuisances. SystemJS is a strictly for aggregating and packaging modules:

“Universal dynamic module loader — loads ES6 modules, AMD, CommonJS and global scripts in the browser and NodeJS”

Implementing the minutia of task running is accomplished by other tools, such as Gulp. While WebPack may seem like an “auto magic” one stop shop approach, I have found this blissful simplicity can turn into an epic meltdown, when things go awry. I have used both tools, and for now I am sticking with SystemJS.

Minko Gechev’s angular-seed provides a productive tool set that is fairly stable and well documented. Whenever I have a need to make modifications for configuring certain task or including 3rd party libraries the process has been for the most part mundane. Part of the tool set includes BrowserSync for running the app in a browser with ‘live-reload’.

I downloaded the Gechev’s angular-seed as a zip file, and copied files over to as needed for my project folder. The root directory appears as such:

Most of my customizations for SASS and inclusion of libraries all takes place in the tools/config/seedconfig.ts and tools/config/project.config.ts. However, I did find it necessary to create a task for unit testing, tools/tasks/seed/build.js.mockdata.ts, which instructs the scaffolding to compile and copy the testing data to the eventual “dist” directory for unit testing. I also added some attributes to the tools/env/env-config.interface.ts, to provide values for host, api name identifier, and ports, which are mostly used in my Docker eco-system.

App Directory Configuration

The Gechev’s angular-seed is a vanilla starter kit for modern Angular project, and thus, does not include a couple of resources that I will use in this project.

  • NGRx (resources) : handles the redux plumbing for modern Angular Projects.
  • angular-material2 : Google’s Material design for modern Angular projects.
  • angular/flex-layout : a library of directives for using css flex.
  • @types/validator: providing some extra validations attributes for our reactive forms. (this is a Definitively Typed package, which is a TypeScript wrapper around a javascript library, in this case validator.js )

After adding these resource as dependencies in our package.json and including them in our seed.config.ts we are good to go.

src/client : contains all directories and files formulating the functional representation of application.

src/e2e : contains e2e (End To End) testing with protractor.

src/test-artifacts : contains data and ancillary files used for unit testing.

src/vendor : contains third party code that does not fit into the paradigm of our tools structure. In this case, I have included the direct download of Animate-sass library from GitHub.

src/ directory and file structure and src/client / directory and file structure

The src/client/app/ directory represents the procedural code base of the application, and it is here where we will instantiate our Angular framework by bootstrapping the root module.

src/client/app/app-stage: centralizing the files necessary for standing up the entire application.

src/client/app/shared-utils/app-env: contains file(s) implementing the specifics of the environment in which the application instantiates. It works in conjunction with the files in the tools/env.

src/client/assets : contains any artifacts and images used with in the application.

src/client/scss: contains customized sass (ex: variables, mixins, abstract classes, etc.,) used to augment css styling, which is for the most part structured with in the angular-material2 theme included as a dependency in our application.

src/client/css : contains are seed-main.css file, implementing reset, and is imported by the main.scss file which acts as the staging for importing files in src/client/scss. The basis for this approach laid by Gechev in a Wiki, Working with SASS.

Separating the various concerns of the application as discussed in the article Practical Web Development and Architecture, the structure of the client is grouped accordingly.

View Layer

src/client/app/view-layer : contains files implementing the presentational concerns of the applications.

Directory structure of the view-layer

~/view-layer/modules-by-route : each directory aggregating files needed for a single module that serves as the base for perspective route (URL view). Modules from modules-by-route that are not included in the app.stage.module are self contained units that are loaded when requested through a ‘lazy loading’ approach.

~/view-layer/common-views : each top level directory represents the aggregation of files for a single view component that can shared across the application.

Data Layer

src/client/app/data-layer : implementation of services for remote (HTTP) data transactions as well as the infrastructure for scaffolding our data models in a Redux paradigm.

Directory structure of the data-layer

~/data-layer/api-services : contains a wrapper ( http.wrapper.services) for abstracting the repeated orchestration of HTTP C.R.U.D implementation from each services ( such as user.services ) api request.

~/data-layer/ngrx-data : implements Redux facilitated through NRGx.

  • reducers : are the immutable data objects representing our Redux Store.
  • actions, guards, and effects contain procedures for interacting with the Store.
  • ngrx.data.module.ts : aggregates these resources for export into our app.stage.module.

*For a more detailed and nuanced understanding of how NGRx facilitates Redux in Angular, the ng-book blog tutorial, “Introduction to Redux with TypeScript and Angular 2, offers one of the best overviews I have come across. Even though it is a bit dated, the principals still apply. To keep up to date with how the progressive versions of Angular as applied to NGRx, follow the example-app Mike Ryan maintains on GitHub.

Business Layer

src/client/app/business-layer : implements a Publish and Subscribe process for brokering request between the view-layer and data-layer, resulting in no direct exchanges between presentational components and the NGRx Redux orchestration.

Directory structure of the business layer

~/business-layer/brokerage : implements transactions with the data-layer by registering as a consumer with the pubsub-broker.

~/business-layer/pubsub-broker : implements a Publish and Subscribe system for interacting with the data-layer.

~/business-layer/models : contains files representing data schemas used by both the view-layer and data-layer.

~/business-layer/shared-types : contains files listing the various action types implemented in the data-layer/ngrx-data/actions against the Store and used by stubs in the brokerage/ngrx-stubs.

~/business-layer/validator : contains an encapsulated service implementation for validation of User input with in forms surfacing in the view-layer.

Data Management and Processing

data-layer: Implements all HTTP processing for requesting and responding to data from the server . It also manages the State of the data using a Redux pattern imposed by NGRx, which formalizes the way data is accessed through Actions and Selectors.

business-layer: Uses PubSubBroker in conjunction with a Brokerage to respond to view-layer demands for access to the data-layer, thus removing any references to the data-layer’s Redux Store in the view-layer. The Brokerage stubs limit what each view-layer component can consume from the Store. This layer is also where encapsulated services for Non-Store related data processing are located.

view-layer : Contains viewable components created with the singular responsibility of presenting data output and consuming User input.

Generating a User Profile View in 7 Steps

The following scenario captures the flow of requesting a User’s profile data for display in the view-layer’s ProfileComponent. It demonstrates a typical scenario a view-layer employs with the PubSubBroker in the business-layer to observe and request action against global State data from the data-layer.

data flow from from view-layer to business-layer t0 data-layer

1When a view-layer component such as the ProfileComponent with data dependencies is instantiated, it send the named identity of a Broker Stub, BROKER_PROFILE_STORE, by way of the PubSubBroker’s , BrokerDispatcherService function dispatchBrokerSelector().

export class ProfileComponent implements OnInit, OnDestroy {
subscribeHandler:any;
userProfile$:Observable<any>;
brokerRef:any;
//BrokerDispatcherService
constructor( private activatedRoute:ActivatedRoute,
private bDS:BrokerDispatcherService) {

var brokerResponse:BrokerResponse =
this.bDS.dispatchBrokerSelector(BrokerList.BROKER_PROFILE_STORE);
this.brokerRef = brokerResponse.brokerRequested;

}
....

2 BrokerDispatcherService shares an established relationship with a Broker Consumer, such as NGRxBrokerConsumer, through a BrokerPublisher. In this case, the NGRxBrokerConsumer function ReceivedBrokerSelectorRequest() uses the named identity to reference the broker stub, BrokerProfileStore.

export class NGRxBrokerConsumer implements IConsumer{  constructor( private brokerDialogStore: BrokerDialogStore,
private brokerLoginStore: BrokerLoginStore,
private brokerNavStore: BrokerNavStore,
private brkrRgstrtnStore: BrokerRegistrationStore,
private brokerProfileStore: BrokerProfileStore ){ }

....

public ReceivedBrokerSelectorRequest(brokerType:string):
BrokerResponse{
var brokerResponse = new BrokerResponse();
switch(brokerType){
....
case this.brokerProfileStore.brokerLabel:
brokerResponse.brokerRequested =
this
.brokerProfileStore.getComponentSupplies();
break;
}
return brokerResponse;
}

}

3 The BrokerProfileStore stub responds to the function call getComponentSupplies() by returning object containing properties for ‘storeObs’ and storeDsp’. The storeObs contains Observables that are derived from the NGRx Store Selectors. The storeDsp contains a set of BrokerActions to dispatch actions against the Store.

export class BrokerProfileStore {
brokerLabel:string = BrokerList.BROKER_PROFILE_STORE;
constructor( private store: Store<fromRoot.State>,
private brkrActnBuilder:BrokerActionBuilder ) { }
getComponentSupplies():any{ return
Object
.assign({
brokerLabel: this.brokerLabel, storeObs:{ userProfile$: this.store.
select(fromRoot.getSelectedProfile)
},
storeDsp:{ GET_USER_PROFILE_ATTEMPT: this.brkrActnBuilder.
create( ProfileActionTypes.GET_USER_PROFILE_ATTEMPT,
this.brokerLabel, null)
}
});
}


}

4 Upon receiving the BrokerResponse, the view-layer component extracts a subscriptions to the Observables contained in the ‘storeObs’ property, which provides a continual awareness of the the State of the userProfile$ attribute in the Store.

//Profile Component continued..
ngOnInit() {

this.userProfile$ = this.brokerRef.storeObs.userProfile$;
.....
}

5 Actions against the Store that the ProfileComponent can perform are contained in the ‘storeDsp’. Request are sent through the PubSubBroker’s BrokerDispatcherService method dispatchBrokerAction() . In this case, the name of a User, supplied as a parameter attribute with in the URL, is sent as an Action.

//Profile Component continued..
ngOnInit() {
...
this.subscribeHandler = this.activatedRoute.params.
subscribe(params => {
var note = this.brokerRef.storeDsp.GET_USER_PROFILE_ATTEMPT;
note.payLoad = params['username'];
this.bDS.dispatchBrokerAction(note);
});
}

6 Once again NGRxBrokerConsumer will consume the request. However, as it is a demand for Action, the request will be received by the ReceiveBrokerAction method, which will then pass the request on to the BrokerProfileStore stub using its dispatchAction() function.

export class NGRxBrokerConsumer implements IConsumer{

....

public ReceiveBrokerAction( brokerAction:BrokerAction){
switch(brokerAction.brokerType){
...
case this.brokerProfileStore.brokerLabel:
this.brokerProfileStore.dispatchAction(brokerAction);
break;
}
}

7 The Broker will use its established reference to the Store to dispatch a new Action ‘profileActions.GetUserProfileAttempt ’ against the State of the Store.

@Injectable()
export class BrokerProfileStore {
brokerLabel:string = BrokerList.BROKER_PROFILE_STORE;
constructor( private store: Store<fromRoot.State>,
private brkrActnBuilder:BrokerActionBuilder ) { }
dispatchAction(brokerAction:BrokerAction):void {
switch(brokerAction.actionType){
case ProfileActionTypes.GET_USER_PROFILE_ATTEMPT:
this.store.dispatch(new
profileActions.GetUserProfileAttempt(
brokerAction.payLoad));
break;
}
}

Exact implementation of what happens once the ProfileAction GetUserProfileAttempt() functions is invoked will be detailed later in the section Processing global State data with NGRx.

Deconstructing PubSubBroker & Brokerage

Within the business-layer, the PubSubBroker and Brokerage module work to process communication from components in the view-layer to the core data objects in the data-layer. Processing communication between the two separate layers with PubSub Broker facilitates a total decoupling of the mechanisms for handling the State of our data object. If our data-layer should change to a different pattern for maintaining State, perhaps no longer using a Redux Store, none of the visual components need to change as long as the new data-layer provides Observables for subscription to the State of data objects, and implement a mechanism for updating the State of data object with some semblance of an Action object.

The PubSubBroker (README.md) module is implemented through the object oriented facilities of TypeScript.

  1. An Abstract class, AbstractBrokerTrader, extends ISupplier and has an array attribute of type IConsumer.
  2. As an abstract class, AbstractBrokerTrader, can not be instantiated, but must be extended by another class which is performed BrokerPublisher .
  3. Since BrokerPublisher extends AbstractBrokerTrader, it is not only an ISupplier, but also possesses an awareness of the IConsumer array attribute type.
  4. The Brokerage module has a ‘registries’ directory, that contains services like NGRxBrokerRegistrationService. These services implement a registration between Brokerage IConsumers like NGRxBrokerConsumer , whose class definition implements IConsumer, and the BrokerPublisher.
  5. This registration is performed when NGRxBrokerRegistrationService, calls the BrokerPublisher function RegisterBrokerConsumer(). The BrokerPublisher function pushes instances of IConsumer types like NGRxBrokerConsumer into its IConsumer array.
  6. The BrokerDispatchService injected into components residing in the view-layer contains a reference to the BrokerPublisher, and thus can invoke BrokerPublisher functions that can reference any of the IConsumer array entities functions like ReceiveBrokerAction().

The only Brokers that currently exist are used to supply the State and provide Actions against the NGRx state management apparatus in the data-layer, other Brokers could be created in the same manner if an alternative pattern or management system is desired.

The Brokerage module directory, ngrx-stubs, contains the various Broker stubs used to gather the Selectors and Actions needed by particular components in the view-layer. Each stub has a a direct reference to the NGRx Store in the data-layer and is instantiated in the business-layer by the NGRxBrokerConsumer.

Processing global State data with NGRx

Flow of Data between actions > reducers > effects

(each step is represent by a number is a yellow circle)

0 Components subscribe to changes in a Store’s object State, (often called Smart Component or Containers) through the use Selectors to establish an Observable, which in the above diagram is label Target.

// While this is often place in the ngOnInit() or constructor() of // Angular components, this call resides in 
// brokerage/ngrx-stubs/broker.profile.store
this.store.select(fromRoot.getSelectedProfile)

1The initiation of the data cycle begins when a new Action is dispatched.

this.store.dispatch(
new profileActions.CheckUserProfileNameAttempt('wilSonic')
);

In the ‘this.store.dispatch’ statement above , the new Object created is configured in an Action declaration file, profile.action.ts .

export class CheckUserProfileNameAttempt implements Action {
public readonly type = ProfileActionTypes.CHECK_USER_PROFILE_NAME_ATTEMPT;
constructor(public payload:string) { }
}

2 Dispatching the Action to the Store, it first passes through a Reducer checking for a ‘case’ to interact with the Action type. If the State of the Reducer changes base on the conditions set within the case , all Observable actively subscribing by way of Selectors, will notified of the change. (… such an Observable was set up in step zero)

The specific action ‘CHECK_USER_PROFILE_NAME_ATTEMPT’ , which initiated our course of action does not have a case in any of the Reducers. However, it will eventually get picked up by an Effect described in the third step.

export  interface State {
ids: string[];
entities: { [id: string]: UserModel };
selectedProfileId: string | null;
validUserName:string;
}

export const initialState: State = {
ids: [],
entities: {},
selectedProfileId: null,
validUserName:null
};
export function reducer(state = initialState, action:
profileActions.Actions): State {
switch (action.type) {
.....

case ProfileActionTypes.SET_SELECTED_PROFILE_ID:{
if (state.ids.indexOf(action.payload) > -1) {
return Object.assign({},
state,
{ selectedProfileId:action.payload });
}else {
return state;
}
}

default: {
return state;
}
}

3Like Reducers, Effects can register a request to interact with specific Action types as well. Thus, our originally dispatched Action continues its course as ProfileEffects has registered a request to interact with the Action type CHECK_USER_PROFILE_NAME_ATTEMPT.

export class ProfileEffects {@Effect() getUserByName$ = this.actions$
.ofType(profileActions.ProfileTypes.CHECK_USER_PROFILE_NAME_ATTEMPT) .map(action => action.payload)
.switchMap(payload =>
this.userServices.getUserByName(
payload,
errorActions.ErrorTypes.REPORT_ERROR,
profileActions.ProfileTypes.CHECK_USER_PROFILE_NAME_FAILURE,
profileActions.ProfileTypes.CHECK_USER_PROFILE_NAME_SUCCESS)
);

constructor( private store:Store<fromRoot.State>,
private userServices: UserServices,
private actions$: Actions
) { }
}

4While simplistic processing of business rules can be applied through manipulation of the Observable using the RxJs reactive library , it is often the case that mapping the Observable to a separate service for processing is a better approach. This is especially true when asynchronous services are need. We use such a service in the above Effect by implementing the UserService ( this.userServices.getUserByName(…) ) to request API information.

export class UserServices {

constructor(private httpWrapperService: HttpWrapperService) { }

getUserByName( payload:string,
ErrorActionType:string,
SpecificErrorType:string,
SuccessType:string) {
let getParams: HttpParams = {
auth: false,
errorActionType: ErrorActionType,
specificErrorType:SpecificErrorType,
payload: payload,
responseObject: 'user',
successActionType:SuccessType,
uri: 'Users/username/'+payload
};
return this.httpWrapperService.get(getParams);
}
}

While UserService methods request HTTP transactions, the actual AJAX is defined in the a wrapper HttpWrapperService(). The results of a successful HTTP request is mapped to the current Observable being processed by the ProfileEffect. In other words, the Observable stream is still active when a successful result from the HTTP request occurs. When there is an error, the current Observable is no longer viable. Like resolving a Promise by creating a Promise.reject, a new Observable must defined as is the case in the catch() statement where Observable.of() is used map the resulting ‘error action.type’.

export class HttpWrapperService {

constructor(private http: Http) { }
...public get(params: HttpParams) {
let {apiUrl, options} =
this.configRequest(params.uri, params.auth);
return this.http.get(apiUrl, options)
.map(res => ({
type: params.successActionType,
payload: res.json()[params.responseObject]
}))
.catch(res => Observable.of({
type: params.errorActionType,
payload: {
action_type: params.specificErrorType,
message: res.json()
}
}));
}
...

5No matter what kinds of procedures occur during the course of the Effects task, the result of the process will be an Action object Observable, which will once again go through the cycle of Redux .

.map(res => ({
type: params.successActionType,
payload: res.json()[params.responseObject]
}))

The above snippet of code demonstrates mapping the result to an a Profile Action type ‘CHECK_USER_PROFILE_NAME_SUCCESS’, which creates the Action object below:

export class CheckUserProfileNameSuccess implements Action {
public readonly type =
ProfileActionTypes.CHECK_USER_PROFILE_NAME_SUCCESS;
constructor(public payload:UserModel) { }
}

While the unsuccessful attempt, will result in the Error Action Type : ‘REPORT_ERROR’ which will create the following Action object:

export class ReportError implements Action {
public readonly type = ErrorActionTypes.REPORT_ERROR;
constructor(public payload: ErrorModel) { }
}

Encapsulated Services:

Finally, there is an instance of Angular’s Reactive Forms used with in the application when the user inputs data in the registration form. The process of validating a user’s input data has been augmented by a set of validation schemas from validator.js. This ‘micro’ process does not utilize NGRx Redux in any significant way other than to check if the ‘username’ is already in use.

Processing data input through validation services

In short, the Registration module imports the Control Message Components and Validation services to display Error Messages below input boxes. Upon entering text and hitting the tab button or just moving focus to the next input box, the data from the previous input box is processed by the validation services. If an error is captured during the validation process,

*ngIf="errorMessage !== null"

Angular’s *ngIf conditional in the Control Message Component template becomes true, and will displays the message.

Hopefully this application demonstrates how a modern Angular project can be structured based on a “separation of concerns”. While the architecture may seem a bit complex for the menial tasks performed (registration and log in), the goal of this effort is to provide an example of a structure for large applications. Whether created and maintained by one developer or a few developers, the concepts outlined in this article can provide an example of a constructive approach toward developing a foundation for maximum scalability.

--

--

Willie Streeter
will.streeter

I am a builder and building is my passion. I have spent the majority of my professional career expressing this passion through the medium of web development.