Hey there. This is part two in a series of undefined length. See what I did there?
In part one I gave a little intro and explained that I'm trying to write these blogs with as much detail in each step (including the issues you will inevitably run into) as possible. I'll try to be consistent in format so you can learn the pattern and jump to what interests you easier as time goes on. If you want to play with the code you can find it here.
Previously, we got the sample angular app that comes with the cli up and running and got our tests running. Now we'll add some kind of functionality to our app. I want to get as many bells and whistles going ASAP so we can grow the app uniformly instead of adding libraries and things along the way so we're going to install a bunch of stuff to start off. I'll explain details about each package as it becomes necessary.
The Goal
- Add a module
- Install and configure an NgRx store
- Write some tests and simple functionality
The Setup
- First let's get NgRx installed by running the following command.
ng add @ngrx/store@latest
I like using the CLI for stuff like this because other nerds have already done the work of updating module files and doing the npm install for us and I'm lazy so they did it specifically for me and who am I to deny their hard work? You can do all kinds of things by passing optional parameters so check out the docs sometime.
- Let's start by running our tests. We'll try some TDD 🙂
The Process
- First some housekeeping. Get rid of the sample template stuff.
- Clear out the app.component.html file. When you do you should see your tests start to fail. Go ahead and clear out the tests for now too. You should be seeing
TOTAL: 0 SUCCESS
now.
- Clear out the app.component.html file. When you do you should see your tests start to fail. Go ahead and clear out the tests for now too. You should be seeing
Now let's make something!
I thought about this for a while and decided I'm going to build something for myself and I guess anybody else that wants it 🙂. I play D&D and am a lazy programmer with terrible handwriting so I want somewhere I can keep track of the events of campaigns. I want to track things like characters we meet, quests we go on or are available to explore, locations we visit, important notes about those locations, and probably tons of other stuff that I haven't thought of. So let's get to it.
Add a module
The main "object" in D&D is the campaign itself so I'm thinking we add a module for the campaign and then we'll add submodules for all the pieces. I'm going to try and use the cli as much as possible because I don't usually and I want to learn it better.
- We'll ask the cli to
g
enerate us amodule
calledCampaign
(the cli will append "Module" onto the name for us). We can also have it do a few other things for us by passing some switches. We'll tell it that- the
--module App
is where the Campaign module will be defined - we want the
--route "campaigns"
to map to the new module. - we want
--routing
to be setup for the new module as well so we can lazy load sub modules
- the
You can read all about the cli options in the docs
ng g module Campaign --module App --route "campaigns" --routing true
If you've followed along up to this point and start the app then you should see the "campaign works!" string in the browser... right? That's what we want.
Here's what actually happened for me... I see nothing. Then I remembered that we deleted all the template stuff out of the app component so it's empty. Here's what I did next...
- Added "App works!" to the app.component.html and then that showed up but the "campaign works!" string still wasn't showing.
- Remembered we have to add the
<router-outlet></router-outlet>
to the app component template. But that still didn't do it... - Looked at the browser and saw that the url was
http://localhost:4200/
but I knew that I wanted the/campaigns/
route to map to my Campaign component so then I also knew I had a routing issue.
We have two routing modules right now, one at the App level and one at the Campaign level. Since we're just at the root of our app right now, let's look at the root routing module app-routing.module.ts
.
If we inspect the routes that the cli set up for us we can see that we indeed got the mapping we asked for but the issue is that we're not routed to that route by default.
- Added an entry to the array telling the router that if the full route (i.e. all the stuff after the localhost part) in the browser is empty then send the request to the campaigns route. The "campaign works!" string started showing up after this last change.
{ path: '', redirectTo: 'campaigns', pathMatch: 'full' }
Add our first feature component
I'm going to add some interfaces and some factory functions for building fake objects in our tests and then I'll make a new component for creating campaigns with the following command. You can check out the details of the files in the repo if you're interested.
-
g
enerate ac
component called NewCampaign under them
odule Campaign and set thec
hangeDetection policy to OnPush-
Note: the cli will add a prefix to component selectors by default. You can customize this with the
--prefix
switch and passing a string or you can use the--selector
switch to explicitly set the entire selector string. I'm removing the defaultapp-
prefix.ng g c NewCampaign -m Campaign -c OnPush --prefix ""
-
Let's add the
<new-campaign></new-campaign>
element to the CampaignComponent template and now we should see our "new campaign works!" string.
TDD time.
We'll try to emulate the fail, refactor, pass pattern for TDD. We'll write a failing test and then refactor code until it passes and then just keep doing that over and over.
When you create a component with the cli Angular will add a test file for you with the standard jasmine syntax. I'm going to use Spectator so this:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewCampaignComponent } from './new-campaign.component';
describe('NewCampaignComponent', () => {
let component: NewCampaignComponent;
let fixture: ComponentFixture<NewCampaignComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NewCampaignComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NewCampaignComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
will become this:
import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { NewCampaignComponent } from './new-campaign.component';
describe('NewCampaignComponent', () => {
let spectator: Spectator<NewCampaignComponent>;
const createComponent = createComponentFactory({
component: NewCampaignComponent
});
beforeEach(async () => {
spectator = createComponent();
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
});
Spectator just takes care of a lot of boilerplate code that gets repeated in practically every test.
You can get some nice syntactic sugar and utilities for your tests by installing Spectator which is made by the fine folks at ngneat. Install it like so. Again, I'll explain things as we go and as we need.
npm i @ngneat/spectator
I have mixed feelings about libraries like this because they can obscure what's going on under the sheets and then when things go wrong it can be even harder to figure out the problem. With that in mind, I still find the convenience that Spectator provides sufficient to accept the risk.
- Let's write a test for our NewCampaign component. I want a field for every property on my model so let's start there. Here's my first test:
it(`should have a firstName field`, () => {
const firstNameField = spectator.query('input.campaign-name');
expect(firstNameField).toBeTruthy();
});
Adding an f
to the front of the describe
in a spec file f
ocuses the test runner on that test. This will reduce the noise a bit as time goes on. What I see in my console is this:
So we got the first step done. Failing test ✅. Now all we have to do is make it pass. As an aside, one of the reasons I like TDD is that it drives your design process from a consumer perspective. More on that later.
- Let's add an input with a class called
campaign-name
. Adding this to the template turns our test green.
<input type="text" class="campaign-name" [placeholder]="'Enter a Campaign Name'" />
Refactor Code ✅
Passing Test ✅
- We'll repeat this process for the other properties of the model by first writing a test that fails and then adding the code to make it pass. Try this with a couple more model properties for funsies. Then let's pause on this and switch gears for a moment.
Wiring up the NgRx store
I mentioned bells and whistles earlier so let's get our store set up and some peripherals we use for our development process.1 When we ran the ng add
command at the very beginning of this post the cli did some housekeeping for us like updating the app.module.ts to import the StoreModule.forRoot()
. We need to do a few similar things before we're ready to roll with the NgRx store. I'm not going to cover how Angular module factories and injectors work or how NgRx works but if you have questions just post them below and I'll do my best to help out.
If you use NgRx you should consider using entity state. This is nice for managing state in NgRx. Again, post any questions below.
ng add @ngrx/entity@latest
Just setup enough of the store so that it loads. The minimum is:
- Add
StoreModule.forRoot({})
to the imports array of the App module - Add
StoreModule.forFeature()
to the imports array of the Campaign module- Create a campaign reducer and add it to the
forFeature()
arguments list
- Create a campaign reducer and add it to the
If you have the redux dev tools2 setup in your browser you should see the store with your state there now kind of like this:
The Sausage
We got a module created and a new component added inside that module. We also got our NgRx store setup and have an action that we can dispatch from our component and a corresponding reducer function that will allow us to persist data to the store. And we even have some tests. 🙂
The Credits
We've come a long way already! Much of the work we've done up to this point has been foundational stuff but in the next episodes we'll start having more fun by spending more time building helpful features and more testing all along the way. I'll try to dedicate attention to this blog more often so that there aren't such big gaps between posts but life is busy and it doesn't seem to be slowing down.
As always, I welcome questions, discussions, and suggestions for the types of features you'd like to see implemented in the app.
Thanks for spending some of your time with me. Be Well.
-
I'm showing how we do things at NHA in this project. It's arguable that NgRx is overkill for this project right now but there are other benefits besides scaling. Maybe we can have a post sometime where we could manage local subjects and observables instead of using NgRx store. ↩
-
Pro Tip: If you're going to work with NgRx do yourself a favor and get the Redux Dev Tools extension for your browser. You won't regret it. Also have a look at the docs because you have to make a change in your module file in order to use the tools. ↩
Top comments (0)