This article series will show how to create an application using Angular 2 or later, and leverage ngrx store for application state. The back-end REST layer will leverage Microsoft Asp.Net MVC 5 / Web API. All of this is backed by a Github repo with all the source code. This project is written with Visual Studio Community as an MVC web application, and uses NPM, Gulp, SystemJS, and NuGet. The “app” angular code folder could be easily copied and used within a Webstorm, VS Code, and other environments without modification. The front end angular code is structured following the angular style guide.
In the last article of this series we created an ngrx store including the state interface our app needs, actions defining state transitions, reducers to modify the state, and glued it all together by registering it in the main app module.
The structure of this app includes a top level person component which is a tabbed interface with the first tab for searching, the default tab when the app is loaded. When search is initiated the results appear on the search tab below the search form. When a result is clicked on the detail view appears in another tab. The tab strip also includes a plus button to add a new item which shows in a new tab.
Duplicate tabs for the same item are not allowed to avoid confusing the user, but you can have as many add new item tabs as you wish. Upon save of an item the presentation of that item in the search results should also update, and further clicks on that result should not get confused by opening a new tab if the item is still open from the initial tab, but rather that original tab should activate instead.
The application consists of more than one routed state in a menu at the top of the page (think of them as separate pages). If you navigate away from one routed state to another and return again the same tab should be open as if you had never left. This is a Single Page Application (SPA) so you don’t really ever navigate away from the page. If you wanted to accomplish the same thing with a multi-page application you may want to also store changed state in isolated storage, which is beyond the scope of this article.
Now that we have an overview of the application structure we can begin to build the pieces.
There are really 3 main routed pages in this application, meaning there are 3 pages navigated to by the angular router. These are all included in one Single Page Application (SPA) put together by AppComponent and managed by AppModule. This article is going to focus on the structure of just one of these routed main components, the person component.
At the top we start with a component class with an injected ngrx store, and this component will coordinate the tabs, including a fixed search tab, and a variable number of person tabs the user has opened. Start by importing the types to be used in this file including some angular basics, our model types, and our store actions used in this file.
import { Component } from '@angular/core'; import { Observable } from 'rxjs/Rx'; import 'rxjs/rx'; import { Store } from '@ngrx/store'; import { Tab } from '../app-shared/tabset/tab.model'; import { Person, PersonHolder } from './person.model'; import { AppState } from '../app.store'; import { PersonCloseAction, PersonOpenAction, PersonTabActivateAction } from './person.store';
Then the definition of the component is relatively straight forward, except notice that the private fields are mostly Observables into the current state of the store.
@Component({ selector: 'app-person', templateUrl: '/dist/js/person/person.component.html' }) export class PersonComponent { openItems: Observable<PersonHolder[]>; tabs: Observable<Tab[]>; activeTabId: Observable; searchTab: Tab; constructor(private _store: Store<AppState>) { this.searchTab = new Tab(); this.searchTab.id = 'tab-id-search'; this.searchTab.title = 'Search'; this.searchTab.template = 'search'; this.searchTab.active = true; this.searchTab.closeable = false; this.searchTab.itemId = ''; this.activeTabId = _store.select(x => x.person.activeTabId); this.openItems = _store.select(x => x.person.openList); this.tabs = this.openItems .combineLatest(this.activeTabId) .map(([people, activeId]) => { this.searchTab.active = activeId === this.searchTab.id || !activeId; return [this.searchTab].concat(people.map(item => { let exists = item.Person && item.Person.PersonId && item.Person.PersonId.length > 0; let t = new Tab(); t.id = item.PlaceholderId; t.title = exists ? item.Person.LastName + ' ' + item.Person.FirstName : 'Add Person'; t.template = 'item'; t.active = activeId === t.id; t.closeable = true; t.item = item.Person; t.itemId = item.Person.PersonId; return t; })); }); } closeTab(tab: Tab) { this._store.dispatch(new PersonCloseAction(tab.id)) } addTab(b: Boolean) { this._store.dispatch(new PersonOpenAction(new Person())); } activateTab(id: string) { this._store.dispatch(new PersonTabActivateAction(id)); } }
The functions at the bottom are invoked by the view template when buttons, icons, or tabs are clicked. If you used local state instead of ngrx you might update a local Tab array instead of the one line call each to the store as shown here.
The constructor setting up the observables is the most complicated part of this component. In many cases your store might contain the exact data you need to build your interface and each of them would be a simple store select call, as shown for the ‘openItems’ field initialization. However, this component wants to display a set of tabs, some for existing items with an id, and others for new items and no id set yet. Additionally, the tabset always wants a fixed ‘search’ tab at the start of the tab list. But since the tab control takes a single list of tabs, the fixed tab needs to be merged into the observable list of open items.
This sample shows how you might combine two observables to update a single computed list and how you can mutate one type into another using values from the other observable supplied by combineLatest, and then passed into map. Lets break this down into more detail.
There are two calls to map in the code above, the outer one has an anonymous method with one array argument containing 2 items.
.map(([people, activeId]) => { ... }
These items in the array are the types observed by the two combined observables. The purpose of this outer map call is to mutate the two values of the observable, which is an array of PersonHolder and a single string. This map call is not to iterate over each person. In essense we are mapping the values in the observable into one different output type to be wrapped into another observable.
Within the outer map call we now need to merge our single search tab item into a new list of tabs to be created from the list of PersonHolder. Translating that concept to code means you want to concat two lists, the first being a list of one static item and the second being a new list to be mapped from the list of people.
return [this.searchTab].concat(people.map(item => { ... }
Now this inner map call is the simple map you know and love that builds an array by converting items in the array to another type. The rest of the logic to actually map a person to a tab is rather mundane. We are just calculating simple values such as title, the icon to the left of the title, and various attributes such as when it is closeable (include a x icon to the right), and if it is currently active. Whether it is active or not is why we needed to combine in the activeTabId observable, since that determination is a value from state.
The view template utilizes a tabset component, and for each tab can show either a person-search or a person-detail component depending on a template property value in the tab. See the github source for the view contents.
The search child component is similar to the main component in that it injects the store, but it also injects a service to initiate and perform the search REST API call. Ngrx store reducers should never have calls to a REST service, so the call should be initiated in a service that then notifies the store of the relevant state changes.
imports omitted... @Component({ selector: 'person-search', providers: [PersonService], templateUrl: '/dist/js/person/search/person-search.component.html' }) export class PersonSearchComponent { results: Observable<Person[]>; searching: Observable; hasResults: Observable; constructor(private _service: PersonService, private _store: Store<AppState>) { this.results = _store.select(x => x.person.results); this.searching = _store.select(x => x.person.isSearching); this.hasResults = _store.select(x => x.person.hasResults); } public criteriaReset(reset: boolean): void { this._store.dispatch(new PersonSearchAction(new PersonCriteria())); } public criteriaSubmitted(criteria: PersonCriteria): void { this._service.search(criteria); } public itemSelected(item: Person) { this._store.dispatch(new PersonOpenAction(item)); } }
The store we inject here is the same as application wide, the top level AppState. Then in the store select calls you can navigate down do the sub-state that you need. This component could use some refactoring to take advantage of smart/view pattern, which is in the plans for this project.
Resetting criteria and selecting an item simply trigger state changes which are then listened to by other components. The criteria submit function needs to trigger a REST API call, and so lets drill into the service to see how that works with state. But first notice this component does not wait for a response to the service call, or have an observable return from the service call. Prior to ngrx this component would subscribe to a returned observable to update its local results. Check out the history of this file in the github source to see what it looked like before ngrx store.
The service is registered as an Injectable class to perform shared logic that may be shared by multiple components. In this case the service makes REST API calls and updates state accordingly. Prior to implementing ngrx this project instead would return an observable from each function that returned data when the REST call returned data. Now the functions do not return data and instead update state as needed, which is observed by the components.
The outline of the service is standard for a service, but we are also injecting the store. The import are omitted from below, you can find them in the github source.
@Injectable() export class PersonService { private baseUrl: string; private headers: Headers; constructor(private _http: Http, private _configuration: Configuration, private _store: Store<AppState>) { this.baseUrl = _configuration.Server + _configuration.ApiUrl + 'person'; this.headers = new Headers(); this.headers.append('Content-Type', 'application/json'); this.headers.append('Accept', 'application/json'); } //functions to be added here, see below }
Prior to ngrx the search function looked like the following:
public search(criteria: PersonCriteria): Observable<Person[]> { return this._http.post(this.baseUrl + '/search', criteria, { headers: this.headers }) .map((response: Response) => <Person[]>response.json()) .catch(this.handleError); }
This function was subscribed to in the person search component, but now with ngrx the search function just subscribes to the result of the API call itself and then updates state.
public search(criteria: PersonCriteria) { this._store.dispatch(new PersonSearchAction(criteria)); this._http.post(this.baseUrl + '/search', criteria, { headers: this.headers }) .map((response: Response) => <Person[]>response.json()) .subscribe(payload => this._store.dispatch(new PersonSearchCompleteAction(payload)), error => { }, //TODO: call another search failed action?? () => { } //on complete ); }
This code is a little longer now that it does both the API call and doing something with the results. Its not extra code, rather it’s code moved from the component into the service.
The save function is similar to search, but note how it calls a different state change based on if the save is an insert of a new item or update of an existing item.
public save(item: PersonHolder) { let isUpdate = item.Person.PersonId; return this._http.post(this.baseUrl + '/', item.Person, { headers: this.headers }) .map((response: Response) => <Person>response.json()) .subscribe(result => { item.Person = result; isUpdate ? this._store.dispatch(new PersonUpdateAction(item)) : this._store.dispatch(new PersonInsertAction(item)) }, error => { }, //TODO: call another save failed action?? () => { } //on complete ); }
The detail component is simpler than before switching over to ngrx store. Previously the save function subscribed to an observable returned by the service and updated the local item bound in the view.
Now after migrating to ngrx store this save function simply calls the save function on the service. When the service completes the save operation updates the store. When the store is updated the parent person component updates the visible tabs and sends the updated Input item with the new value.
import { Component, Input } from '@angular/core'; import { PersonHolder } from '../person.model'; import { PersonService } from '../person.service'; @Component({ selector: 'person-detail', providers: [PersonService], templateUrl: '/dist/js/person/detail/person-detail.component.html', }) export class PersonDetailComponent { @Input() public item: PersonHolder; constructor(private _itemService: PersonService) { } public save(): void { this._itemService.save(this.item); } }
This detail component does not get its data direct from the state since it is only a single item in the list of open items, so it doesn’t have a specific item on the state it can observe. Rather the parent person component observes the list of open items and using an ngFor in the view creates person-detail components and bind in a list item to each.
In this article we learned how to implement ngrx store in your application components. If you were migrating an existing app to use ngrx while reading this, your app should now run again, but with the benefits of ngrx store.
This concludes this article series, but look out for more articles walking though other parts of this sample ride-sharing project.
Full Source code for this article series
https://github.com/michael-lang/sample-ng2-mvc
Build a Better Angular 2 application with Redux and ngrx (Lukas Ruebbelke)
http://onehungrymind.com/build-better-angular-2-application-redux-ngrx/
Managing State in Angular2 Applications (Kyle Cordes)
https://www.youtube.com/watch?v=eBLTz8QRg4Q
How to build a real world Angular app with ngrx store, Part I: Define the state
I really liked how you structured the store actions with the reducers. In a larger application the default pattern of using switch statements gets mighty tedious. I have been using your patterns with effects and agree that having to ‘know’ what action triggers the effect is a downside. Great post! Thank you.
Hello Michael, first of all I congratulate you for the excellent post. I have a question, the right place to dispatch an action is within service o can it within component?
Greetings…..
I’m seeing that there are already 4 comments, but I am unable to see any of them. So, I apologize if this has already been asked.
What are your thoughts about using the ngrx effects to separate the setting of state from the service? You have a following article in which you discuss the use of an orchestration class to do this, but would still like to know what you think of effects?
Thanks for the articles, they are a great read.