Front-end Madness: Angular Material Theme

If you’ve been following along in this series you will have noticed that the UI for our application is pretty bad. My app currently looks like this:bad-ui

This is mostly due to the fact that we haven’t really done anything with CSS. This is something we need to address, so let’s start with setting up our material theme.

Material Theme

Setting up an material theme is actually quite easy thanks to the good people working on Angular material. We need to change our ./src/styles.css to be ./src/styles.scss. This will allow us to use sass and the helper classes in the @angular/material module. Now we need to add the following to our styles.scss:

// styles.scss
@import '~@angular/material/theming';

@include mat-core();

$primary: mat-palette($mat-blue);
$accent: mat-palette($mat-yellow);
$warn: mat-palette($mat-red);

$theme: mat-light-theme($primary, $accent, $warn);
@include angular-material-theme($theme);

This will give us a good starting point for our application theme. However, this will have caused issues with building our application. I now see the following when running the application:

npm start
...


ERROR in ./node_modules/css-loader?{"sourceMap":false,"import":false}!./node_modules/postcss-loader/lib?{"ident":"postcss","sourceMap":false}!./src/styles.css
Module build failed: Error: ENOENT: no such file or directory, open 'C:\dev\code\boot-xp\front-end-madness\angular-app\src\styles.css'
 @ ./src/styles.css 4:14-138
 @ multi ./src/styles.css

webpack: Failed to compile.

If you look closely you can see that webpack is looking for ./src/styles.css obviously this doesn’t exist so we need to update our .angular-cli.json to point to the correct file:

// .angular-cli.json
{
  ...
  "apps": [
    {
      ...
      "styles": [
        "styles.css"
        "styles.scss"
      ],
      ...
    }
  ],
  ...
}

Now we can restart our build and we should see:

npm start
...
** NG Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
Date: 2018-02-27T03:25:49.091Z
Hash: f1ab60557e2834767696
Time: 14716ms
chunk {customers.module} customers.module.chunk.js () 6.19 MB [rendered]
chunk {inline} inline.bundle.js (inline) 5.79 kB [entry] [rendered]
chunk {main} main.bundle.js (main) 21.2 kB [initial] [rendered]
chunk {polyfills} polyfills.bundle.js (polyfills) 557 kB [initial] [rendered]
chunk {styles} styles.bundle.js (styles) 183 kB [initial] [rendered]
chunk {vendor} vendor.bundle.js (vendor) 9.28 MB [initial] [rendered]

webpack: Compiled successfully.

Perfect Lets check out our UI now:

bad-ui-with-theme

Well we must be missing something.

If you dig into the Angular Material docs a little you will find two options:

  • Wrap your application inside a mat-sidenav-container
  • Add mat-app-background class to our <body> element

Since we don’t have a need for a mat-sidenav-container I’m going to opt for adding mat-app-background to our <body> element. To do that we need to edit our index.html:

// index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularApp</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body class="mat-app-background">
  <app-root></app-root>
</body>
</html>

Okay let’s checkout our UI.

bad-ui-with-class-applied

Well our fab button still isn’t correct. We still need to make a few more changes to our index.html:

// index.html
<!doctype html>
<html lang="en">
<head>
  ...
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  ...
</head>
...
</html>

Those two lines give us the material icons and the Roboto font for use in material design. At this point we are closer but we still have a few changes to make. Notice that our button is still not a fab button? To achieve that look we need to go to our CustomersModule and add a few @angular/material modules to our module:

// customers.module.ts
...
import {..., MatIconModule, ..., MatButtonModule} from "@angular/material";
...
@NgModule({
  imports: [
    ...
    MatIconModule,
    MatButtonModule,
    ...
  ],
  ...
})
export class CustomersModule { }

We also need to update the html for our CustomersRootComponent:

// customers-root.component.html
add

</div>

Now we should have an updated UI that looks decent or that we can at least tell what the button is supposed to do.

ui-working-fab

Beautiful we have a working fab button now. However, I would like our components to fill the entire page. Let’s add some CSS to make our app fill the available space:

// styles.scss
...
html,
body,
app-root {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
}

This makes our html and body take up the full page without scroll bars.

Now in order for our list and fab to fall in the correct locations we need to install a new package from npm

npm install @angular/flex-layout --save

This module will handle the majority of the layout in our application hence its name. This module allows you to use directives for setting up layouts using flexbox. Once this module is installed import it into our CustomersModule:

// customers.module.ts
...
import {FlexLayoutModule} from "@angular/flex-layout";

@NgModule({
  imports: [
    ...
    FlexLayoutModule,
    ...
  ],
  ...
})
export class CustomersModule { }

Now that our CustomersModule imports the FlexLayoutModule we can begin using the layout directives to get our list and fab setup correctly:

// customers-root.component.html
fxLayout=”row” fxFill>

fxFlex fxLayout=”coumn”> fxFlex> {{customer.name}} add

</div>

Now our UI looks like this:

ui-correctly-laidout

This is much better than our previous attempt. What does our form look like for creating a customer?

create-customer-save-button-gone.PNG

Well at least the placeholder works, but we are missing our save button. Let’s get that added back in using our new layout and theme.

// create-customer.component.html
<mat-card fxFlex>
  <mat-card-title>
    <h3>Create Customer</h3>
  </mat-card-title>
  <mat-card-content>
    <form [formGroup]="form" (ngSubmit)="save()" fxLayout="column" fxLayoutAlign="start start" fxFlex="33">
      <mat-form-field>
        <input matInput placeholder="Customer Name" formControlName="name" id="name" name="name">
      </mat-form-field>
    </form>
  </mat-card-content>
  <mat-card-actions>
    <button id="createCustomer" mat-button color="primary" type="submit" (click)="save()">Save</button>
  </mat-card-actions>
</mat-card>

These changes will require adding the MatCardModule to the CustomersModule:

...
import {
  ...,
  MatCardModule
} from "@angular/material";
...

@NgModule({
  imports: [
    ...
    MatCardModule,
    ...
  ],
  ...
})
export class CustomersModule { }

Now we should have a much nicer looking create customer form:

create-customer-card.PNG

That looks pretty good for just a few changes. Lets go ahead and see if our end-to-end tests still work:

Customers
 √ should show empty customers
BACKEND: 'GET /api/customers 304 - - 2.251 ms'

BACKEND: 'POST /api/customers 201 - - 10.598 ms'

BACKEND: 'GET /api/customers 200 102 - 2.978 ms'

√ should create a new customer

Perfect. We have a much better UI and our end-to-end tests still work as expected.

CODE CHECKPOINT

Advertisements
Front-end Madness: Angular Material Theme

Front-end Madness: Angular – Create A Customer

Now that we know we have a simple back-end REST api we can add the ability to create a customer. Just like the last feature we added we will start with an end-to-end test that expects the happy path to work.

NOTE: Keep in mind that end-to-end tests are not intended to be exhaustive tests. I generally keep end-to-end tests limited to testing the happy path for a feature.

Create A Customer End-to-End Test

CODE STARTING POINT

The above starting point is where we will begin adding our end-to-end test. Let’s go to our customers end-to-end test and add the following test:

// ./angular-app/e2e/customers/customers.e2e-spec.ts
import {CustomersPage} from "./customers.po";

describe('Customers', () => {
  let page: CustomersPage;

  beforeEach(() => {
    page = new CustomersPage();
  })
...
  it('should create a new customer', () => {
    page.navigateTo();

    const createPage = page.addCustomer();
    createPage.enterName('Jack');
    createPage.save();

    expect(page.getCustomers().count()).toBe(1, 'Customers list does not contain new customer');
    expect(page.getCustomers().getText()).toContain('Jack', 'Customers list did not contain customer with name Jack');
  })
})

I have also extended the CustomersPage object to have a addCustomer method. I’ve also added another class for interacting with the create customer page, CreateCustomerPage. They can be seen below:

# CustomersPage.ts
export class CustomersPage {
...
  addCustomer() {
    element(by.id('createCustomer')).click();
    return new CreateCustomerPage();
  }
}

# CreateCustomerPage.ts
import {by, element} from "protractor";

export class CreateCustomerPage {
  enterName(name: string) {
    element(by.id('name')).sendKeys(name);
  }

  save() {
    element(by.id('createCustomer')).click();
  }
}

This looks pretty good as a starting point. We should be able to run this code and see that we have a failing test. Here is the test failure I see when I run the test:

npm run e2e
...
× should create a new customer
 - Failed: No element found using locator: By(css selector, *[id="createCustomer"])

Good we have a failing test as it can’t find the createCustomer button.

CODE CHECKPOINT

Add Button to Customers List

Since our end-to-end test is failing because we don’t have a ‘createCustomer’ button we need to add this button to our application. I plan to add the ‘createCustomer’ button to the CustomersListComponent. I’ll start with writing a unit test that expects the button to exist.

// customers-root.component.spec.ts
...
import {Router} from "@angular/router";
import {RouterTestingModule} from "@angular/router/testing";

describe('CustomersRootComponent', () => {
  let router: Router;
  ...

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      ...
      imports: [
        // Need to have the router available in the testing module
        RouterTestingModule.withRoutes([]),
        ...
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
        // Need to get the router used in the component
    router = TestBed.get(Router); 
    ...
  });
  ...
  it('should create customer', async(() => {
    // Spy on the router method to verify it is used
    spyOn(router, 'navigateByUrl');

    fixture.detectChanges();
    const req = httpTestingController.expectOne('http://localhost:5000/api/customers');
    req.flush({items: []});
    fixture.detectChanges();

    // Simulate clicking the create customer button
    fixture.debugElement.query(By.css('#createCustomer')).triggerEventHandler('click', null);
    fixture.detectChanges();

    fixture.whenStable().then(() => {
        // Make sure the router actually navigated to the page
        expect((<any>router.navigateByUrl).calls.mostRecent().args[0].toString()).toBe('/customers/create-customer');
    })
 }))
 ...
});

This test looks pretty messy right now, but we will clean it up later.

If we run the tests we should see we have one failing test:

Chrome 64.0.3282 (Windows 10 0.0.0) CustomersRootComponent should create customer FAILED
 Failed: Cannot read property 'triggerEventHandler' of null
 TypeError: Cannot read property 'triggerEventHandler' of null
...
Chrome 64.0.3282 (Windows 10 0.0.0): Executed 4 of 4 (1 FAILED) (0.405 secs / 0.394 secs)

Again here we are failing because there is no element with id ‘createCustomer’. Now we can start adding the code to make the tests green.

I believe we only need to add to the html of the component:

<mat-list class="customers-list">
  <mat-list-item class="customer-list-item" *ngFor="let customer of customers">
  </mat-list-item>
  <!-- Add a mat-icon-button to the html -->
  <button mat-icon-button id="createCustomer" [routerLink]="'/customers/create-customer'"></button>
</mat-list>

Now we should have a passing test:

Chrome 64.0.3282 (Windows 10 0.0.0): Executed 4 of 4 SUCCESS (0.274 secs / 0.268 secs)

Perfect we have a passing test. Now this is where we get into where tests aren’t always the answer.

CODE CHECKPIONT

Now what happens when we run the application and click our button?

RROR Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'customers/create-customer'
Error: Cannot match any routes. URL Segment: 'customers/create-customer'
 at ApplyRedirects.noMatchError (router.js:1719)
 at ...

This happens because we never added our create customer route to our CustomersRoutingModule. Let’s add our new route to the routing module:

...
import {CreateCustomerComponent} from "./components/create-customer/create-customer.component";

const routes: Routes = [
  {
    path: '',
    component: CustomersRootComponent,
    children: [
      // New child route for Customers
      { path: 'create-customer', component: CreateCustomerComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class CustomersRoutingModule { }

In order for this to work we need to generate our CreateCustomerComponent:

ng generate component customers/components/CreateCustomer

Now we need to start working on our CreateCustomerComponent.

CODE CHECKPOINT

Create Customer Component

First let’s add a test to our CreateCustomerComponent for entering and saving the customer:

// create-customer.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { CreateCustomerComponent } from './create-customer.component';
import {By} from "@angular/platform-browser";
import {MatFormFieldModule, MatInputModule} from "@angular/material";
import {NoopAnimationsModule} from "@angular/platform-browser/animations";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing";

describe('CreateCustomerComponent', () => {
  let httpTestingController: HttpTestingController;
  let fixture: ComponentFixture<CreateCustomerComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ CreateCustomerComponent ],
      imports: [
        FormsModule,
        ReactiveFormsModule,
        HttpClientTestingModule,
        MatFormFieldModule,
        MatInputModule,
        NoopAnimationsModule
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    httpTestingController = TestBed.get(HttpTestingController);

    fixture = TestBed.createComponent(CreateCustomerComponent);
    fixture.detectChanges();
  });

  it('should save customer with name', async(() => {
    const input = fixture.debugElement.query(By.css('#name')).nativeElement;
    input.value = 'The Name';
    input.dispatchEvent(new Event('input'));

    fixture.debugElement.query(By.css('#createCustomer')).triggerEventHandler('click', null);

    const req = httpTestingController.expectOne('http://localhost:5000/customers');
    fixture.whenStable().then(() => {
      expect(req.request.method).toBe('POST');
      expect(req.request.body).toEqual({ name: 'The Name' });
    })
    req.flush({status: 201});
  }));
});

Okay this is a lot of code to look at so let’s break it down.

The ‘beforeEach’ code is setting up our test which is pretty much the same as the other tests we’ve written. The interesting part about this test is how we interact with our form.

In our first test we:

  1. Grab the name input element
    fixture.debugElement.query(By.css('#name')).nativeElement;
  2. Change the value
    input.value = 'The Name';
  3. Signal that the input value has changed
    input.dispatchEvent(new Event('input'));
  4. Click the create customer button
    fixture.debugElement.query(By.css('#createCustomer')).triggerEventHandler('click', null);
  5. Verify we created the new customer against our api
    const req = httpTestingController.expectOne('http://localhost:5000/customers');
    fixture.whenStable().then(() => {
      expect(req.request.method).toBe('POST');
      expect(req.request.body).toEqual({ name: 'The Name' });
    })
    req.flush({status: 201});

At this point we should have a failing test because we haven’t created our component at all yet we are attempting to use it. Here is the failure I see:

Chrome 64.0.3282 (Windows 10 0.0.0) CreateCustomerComponent should save customer with name FAILED
 Failed: Cannot read property 'nativeElement' of null
...
 at ProxyZoneSpec.webpackJsonp.../../../../zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke C:/dev/code/boot-xp/front-eChrome 64.0.3282 (Windows 10 0.0.0): Executed 5 of 5 (1 FAILED) (0.475 secs / 0.462 secs)

We need to add our form logic to our component:

...
import {HttpClient} from "@angular/common/http";
import {FormBuilder, FormGroup} from "@angular/forms";

@Component({
...
})
export class CreateCustomerComponent implements OnInit {
  form: FormGroup;

  constructor(private http: HttpClient, private formBuilder: FormBuilder) { }

  ngOnInit() {
    this.form = this.formBuilder.group({
      name: ['']
    })
  }

  save() {
    const customer = { name: this.form.value.name };
    this.http.post('http://localhost:5000/customers', customer)
      .subscribe(() => {});
  }
}

The new part here is the creation of the form using FormBuilder. FormBuilder is one way of creating forms in Angular that utilize a reactive style of programming. For more information about FormBuilder look here. Now that we have our TypeScript code in place we need to add html to our create-customer.component.html:

<form [formGroup]="form" (ngSubmit)="save()">
  <mat-form-field>
    <input matInput placeholder="Customer Name" formControlName="name" id="name" name="name">
  </mat-form-field>

  <button id="createCustomer" mat-button type="submit" (click)="save()"></button>
</form>

The key part of this html is the bindings of the form group and form control name. These two bindings keep our component form in-sync with the user’s input.

At this point we should have a passing test:

Chrome 64.0.3282 (Windows 10 0.0.0): Executed 5 of 5 SUCCESS (0.476 secs / 0.467 secs)

Perfect we have a working unit test. How about our end-to-end test?

1) Customers should create a new customer
 - Expected 0 to be 1, 'Customers list does not contain new customer'.
 - Expected [ ] to contain 'Jack', 'Customers list did not contain customer with name Jack'.

Well we don’t have a working end-to-end test.

CODE CHECKPOINT

Let’s run our app and see what is happening in our application. When I attempt to create a customer I get the following error:

HTTP404: NOT FOUND - The server has not found anything matching the requested URI (Uniform Resource Identifier).
(XHR)POST - http://localhost:5000/customers

Clearly we have the wrong url for posting our new customer. We need to post to http://localhost:5000/api/customers. That’s an easy change however we need to update our spec and our component:

// create-customer.component.spec.ts
...
describe('CreateCustomerComponent', () => {
  ...

  it('should save customer with name', async(() => {
    ...
    const req = httpTestingController.expectOne('http://localhost:5000/customers');
    const req = httpTestingController.expectOne('http://localhost:5000/api/customers');
 ...
 }));
});

// create-customer.component.ts
...
export class CreateCustomerComponent implements OnInit {
  ...
  save() {
    ...
    this.http.post('http://localhost:5000/customers', customer)
    this.http.post('http://localhost:5000/api/customers', customer)
        .subscribe(() => {});
 }
}

No we are posting to the correct url. Let’s run our end-to-end test.

1) Customers should create a new customer
 - Expected 0 to be 1, 'Customers list does not contain new customer'.
 - Expected [ ] to contain 'Jack', 'Customers list did not contain customer with name Jack'.

This is the same error as before however we know the customer is now being added correctly as we just saw that happen. However, when adding the customer manually our application doesn’t return to the customer list. We could solve this one of two ways:

  • Change our end-to-end test to go to the customers list after adding a customer
  • Change our application to go to the customers list after adding a customer

I would like our application to return the user to the customers list after adding a customer.

CODE CHECKPOINT

Returning to Customers List

I believe returning to the customers list should be as easy as telling the router to navigate to the customers list. Lets add a new test:

// create-customer.component.spec.ts
...
import {RouterTestingModule} from "@angular/router/testing";
import {Router} from "@angular/router";

describe('CreateCustomerComponent', () => {
  ...
  let router: Router;
  ...

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ CreateCustomerComponent ],
      imports: [
        ...
        RouterTestingModule.withRoutes([])
      ]
    })
    .compileComponents();
  }));
  
  beforeEach(() => {
    router = TestBed.get(Router);
    ...
  });
 ...
   it('should navigate to customers list after adding customer', async(() => {
     spyOn(router, 'navigateByUrl');

     const input = fixture.debugElement.query(By.css('#name')).nativeElement;
     input.value = 'The Name';
     input.dispatchEvent(new Event('input'));

     fixture.debugElement.query(By.css('#createCustomer')).triggerEventHandler('click', null);

     const req = httpTestingController.expectOne('http://localhost:5000/api/customers');
     fixture.whenStable().then(() => {
       expect(router.navigateByUrl).toHaveBeenCalledWith('/customers');
     })
     req.flush(201);
 }))
});

Our test should fail so let’s check that:

Chrome 64.0.3282 (Windows 10 0.0.0) CreateCustomerComponent should navigate to customers list after adding customer FAILED
 Expected spy navigateByUrl to have been called with [ '/customers' ] but it was never called.

Good we have a failing test so now we can add our navigation to the CreateCustomerComponent:

// create-customer.component.ts
...
import {Router} from "@angular/router";
...
export class CreateCustomerComponent implements OnInit {
  ...
  constructor(private http: HttpClient,
              private formBuilder: FormBuilder,
              private router: Router) { }
  ...
  save() {
    ...
    this.http.post('http://localhost:5000/api/customers', customer)
      .subscribe(() => this.router.navigateByUrl('/customers'));
  }
}

Now we should have a passing test:

Chrome 64.0.3282 (Windows 10 0.0.0) CreateCustomerComponent should save customer with name FAILED
 Error: Cannot match any routes. URL Segment: 'customers'

Well that kind of worked. We fixed our previously failing test but our first create customer test is now failing. I believe what we need to do is move the router spy up to the before each. Let’s try that:

// create-customer.component.spec.ts
...
describe('CreateCustomerComponent', () => {
  ...
  beforeEach(async(() => {
    ...
  }));

  beforeEach(() => {
    router = TestBed.get(Router);
    spyOn(router, 'navigateByUrl');
    ...
  });

  it('should save customer with name', async(() => {
    ...
  }));

  it('should navigate to customers list after adding customer', async(() => {
    spyOn(router, 'navigateByUrl');
    ...
  }))
});

That should fix our failing test:

Chrome 64.0.3282 (Windows 10 0.0.0): Executed 6 of 6 SUCCESS (0.551 secs / 0.539 secs)

Perfect our test now passes.

CODE CHECKPOINT

Now we can see what happens in our end-to-end test:

1) Customers should create a new customer
 - Expected 0 to be 1, 'Customers list does not contain new customer'.
 - Expected [ ] to contain 'Jack', 'Customers list did not contain customer with name Jack'.

Dang same error as before. However, we do end up going back to our customers list. However, it is using the old data. The list isn’t being refreshed when we go back to it.

I believe the issue is related to how our routing is setup. Our current routing has our create customer route as a child of our root route. If we change this to be like:

// customers-routing.module.ts
...

const routes: Routes = [
  { 
    path: '', 
    component: CustomersRootComponent,
    children: [
      { path: 'create-customer', component: CreateCustomerComponent }
    ]
  },
  { path: 'create-customer', component: CreateCustomerComponent }
];
...

Now that we have updated our routing we can see if that fixes our end-to-end tests:

1) Customers should create a new customer
 - Expected [ '' ] to contain 'Jack', 'Customers list did not contain customer with name Jack'.

Nice our end-to-end test is one step closer to being fixed. Now the new customer item is in the view however our list item isn’t displaying the name. To fix this we can add a test to our CustomersRootComponent.

// customers-root.component.spec.ts
...
import {By} from "@angular/platform-browser";
...

describe('CustomersRootComponent', () => {
  ...
  it('should show customer name', async(() => {
    fixture.detectChanges();

    const req = httpTestingController.expectOne('http://localhost:5000/api/customers');
    req.flush({items: [{ name: 'Hello' }] });
    
    fixture.detectChanges();
    fixture.whenStable().then(() => {
      expect(fixture.debugElement.query(By.css('.customer-list-item')).nativeElement.textContent).toContain('Hello');
    })
  }))
  ...
});

This test should fail because our component isn’t displaying the customer name.

Chrome 64.0.3282 (Windows 10 0.0.0) CustomersRootComponent should show customer name FAILED
 Expected '
 ' to contain 'Hello'.

Perfect. Now we can update our component to display the customer name:

// customers-root.component.html
{{customer.name}}

… </div>

With this small change our unit tests should be passing again.

Chrome 64.0.3282 (Windows 10 0.0.0): Executed 7 of 7 SUCCESS (0.751 secs / 0.735 secs)

Great our failing test has passed. Now what about our end-to-end test?

Customers
 √ should show empty customers
BACKEND: 'GET /api/customers 304 - - 1.458 ms'

BACKEND: 'POST /api/customers 201 - - 12.551 ms'

BACKEND: 'POST /api/customers 201 - - 10.623 ms'

BACKEND: 'GET /api/customers 200 102 - 5.280 ms'

 √ should create a new customer

Executed 2 of 2 specs SUCCESS in 4 secs.

Finally we have a working end-to-end test. If you have been running the UI or watching the end-to-end tests you can see that the UI looks awful. Next time we’ll setup angular material to correctly style our application and make it look pretty.

CODE CHECKPOINT

 

 

Front-end Madness: Angular – Create A Customer

Front-end Madness: Simple Back-end

Last time we added a simple look at showing a list of customers to an Angular application. One part that was failing was the http call to the back-end. Since this series is focused on front-end technologies I only want to cover the back-end code briefly.

Express REST Api

The back-end for this series of posts is an expressjs REST api. I created the back-end api before starting on the last post.

CURRENT BACK-END

The back-end code does have a set of tests to ensure it works. The tests are written using mocha and chai. Notice that I didn’t list a mocking framework the reason here is that I simply don’t need to worry about mocking data. The back-end is simple enough I can test the entire thing without worrying about mocks, fakes, or test doubles.

Back-end Tests

To start I want to take a quick look at how the api is tested without mocks, fakes, or doubles of any kind. Since the last post dealt with showing customers in the Angular application I want to take a look at the one of the customers api tests.

# customers.spec.ts
...
describe('Customers', () => {
    let httpServer: http.Server;
    let baseUrl: string;

    before(() => {
        const result = setupServer();
        httpServer = result.httpServer;
        baseUrl = result.baseUrl;
    })

    it('should return empty customers', async () => {
        const result = await getJson<ResultList<Customer>>(`${baseUrl}/api/customers`);
        expect(result).to.eql({ items: [] });
    })
    ...
    after(async () => {
        await tearDownServer(httpServer);
    })
})

This is a fairly simple test and was one of the first tests I wrote. Since I wrote it the code evolved quite a bit. To follow that evolution you can checkout the series of commits that involved this set of specs.

Initial back-end spec ⇒ Refactored Repository/Database ⇒ Lokijs to populate Id ⇒ Break down spec ⇒ Added orders api ⇒ Added products api ⇒ Added api to get orders for customer

Unfortunately, I wasn’t diligent enough at the time to capture more regular snapshots of the evolution of the api.

Notice that in these tests I start and stop the express app before each set of specs. The first question many people would ask is doesn’t that make the tests really slow? To answer that we need to run the tests. To run the backend tests you can use the commands:

cd ./backend
npm install
npm test

On my machine the tests take about < 500 ms. For me this is fast enough I’m able to get quick feedback that the api is working. The feedback I receive from these tests confirms that the express app starts, each endpoint functions as expected, and the lokijs database is read/written correctly.

Running the Back-end

To run the back-end api you can use the following commands:

cd ./backend
npm install
npm start

You should see the following output in your terminal:

Now listening at http://localhost:5000

At this point you should be able to start using something like Postman or curl to start interacting with the api. Here are some examples using curl.

curl http://localhost:5000
curl http://localhost:5000/api/customers
# Powershell curl post
curl -Method POST -ContentType "application/json" -Body '{"name":"workingdev"}' -Uri http://localhost:5000/api/customers

# bash curl post
curl -d '{"name": "workingdev"}' -H "Content-Type: application/json" -X POST http://localhost:5000/api/customers

Since there is no authentication interacting with the api is simple and easy, just what we want for now.

What Apis Exist?

The back-end has more that just customers as part of the api. Below is a listing of the apis that exist. Eventually I’d like to add swagger or something similar to the api but for now this will work:

  • Customers
    • GET /api/customers
    • GET /api/customers/:id
    • GET /api/customers/:id/orders
    • POST /api/customers
    • PUT /api/customers/:id
    • DELETE /api/customers/:id
  • Orders
    • GET /api/orders
    • GET /api/orders/:id
    • POST /api/orders
    • PUT /api/orders/:id
    • DELETE /api/orders/:id
  • Products
    • GET /api/products
    • GET /api/products/:id
    • POST /api/products
    • PUT /api/products/:id
    • DELETE /api/products/:id

Running the Api and Angular App

The root of the repository is an npm package with a set of scripts to help with this. Running both applications can be done using the command:

cd {root of repo}
npm start # starts backend and each app

This is great However, it doesn’t take care of our end-to-end tests scenario. I like end-to-end tests to start up all apis, services, etc. that are needed for the test. To do this we should be able to modify the onPrepare method of the protractor.conf.js. Go to the angular-app directory of the repo and add the following to the protractor.conf.js:

...
const path = require('path');
const { spawn } = require('child_process');

let backendProcess;
exports.config = {
  ...
  onPrepare() {
    ...
    return startBackend();
  },
  onComplete() {
    if (!backendProcess && !backendProcess.killed)
      return;

    backendProcess.kill('SIGTERM');
  }
};

function startBackend() {
  return new Promise((resolve, reject) => {
    backendProcess = spawn('npm', ['start'], { cwd: path.resolve(__dirname, '..', 'backend'), shell: true });
    backendProcess.stdout.on('data', data => {
      if (data.indexOf('Now listening'))
        resolve();

      console.log(`BACKEND: ${data}`)
    });
    backendProcess.stderr.on('data', data => console.error(`BACKEND: ${data}`));
    backendProcess.on('close', code => console.log(`BACKEND: Exited with code ${code}`));
  })

}

The Angular end-to-end tests will now start up the back-end api so that when interacting with the api while running tests will work.

CODE CHECKPOINT

Next we will start adding more components and interactivity to our Angular application.

Front-end Madness: Simple Back-end

Front-end Madness: Angular – Customers

For the first feature in our application I want to add the ability to view customers. I plan to use a master detail type of view.

In order to get started with our features I plan to bring in a few libraries that I hope others will at least look at when building Angular applications.

Extra libraries

To install these libraries we can run:

npm install @angular/cdk @angular/material --save

End-to-End Customers Test

To get started with our master detail list we’ll start by adding an end-to-end test. I like to organize all of my code by feature instead of object type. In light of this preference I’m going to remove the existing end-to-end test and add a new one for customers. Here is our new end-to-end test:

import {CustomersPage} from "./customers.po";

describe('Customers', () => {
  let page: CustomersPage;

  beforeEach(() => {
    page = new CustomersPage();
  })

  it('should show empty customers', () => {
    page.navigateTo();

    expect(page.getCustomersList().isPresent()).toBe(true, 'Customers list is not present on page');
    expect(page.getCustomers().count()).toBe(0, 'Customers list is not empty');
  })
})

This is fairly simple to start. We just expect to see an empty customers list. Since no customers exist this makes sense. Notice that we have added messages to our expectations so that it is easier to figure out what failed when our tests don’t work.

When we run this we will see:

Customers
 × should show empty customers
 - Expected false to be true, 'Customers list is not present on page'.
 at UserContext.<anonymous> (C:\dev\code\boot-xp\front-end-madness\angular-app\e2e\customers\customers.e2e-spec.ts:13:49)
 at new ManagedPromise (C:\dev\code\boot-xp\front-end-madness\angular-app\node_modules\selenium-webdriver\lib\promise.js:1067:7)
 at ControlFlow.promise (C:\dev\code\boot-xp\front-end-madness\angular-app\node_modules\selenium-webdriver\lib\promise.js:2396:12)
 at TaskQueue.execute_ (C:\dev\code\boot-xp\front-end-madness\angular-app\node_modules\selenium-webdriver\lib\promise.js:2970:14)
 at TaskQueue.executeNext_ (C:\dev\code\boot-xp\front-end-madness\angular-app\node_modules\selenium-webdriver\lib\promise.js:2953:27)
 at asyncRun (C:\dev\code\boot-xp\front-end-madness\angular-app\node_modules\selenium-webdriver\lib\promise.js:2860:25)
 at C:\dev\code\boot-xp\front-end-madness\angular-app\node_modules\selenium-webdriver\lib\promise.js:676:7
 at <anonymous>
 at process._tickCallback (internal/process/next_tick.js:160:7)

Now that we have a failing test we can move onto the actual functionality.

CODE CHECKPOINT

Reorganize Root Concerns

With an end-to-end test in place we know what we are trying to make happen. In this case I want to get rid of a lot of the existing code that is unnecessary for our application. I’m going to rename the app.component.* files  to root.component.*. I’m also going to move those into a Root folder to indicate they are part of the root application and not a specific feature. I’ve bounced back and forth between Root and Shell, but have settled on root as most people seem to understand that. I’m also going to move the app.module.ts file into the root folder and rename it to root.module.ts. This keeps all of the root level concerns in one location for easy identification.

The above changes aren’t absolutely necessary, but it helps me keep things clean and easy to find.

CODE CHECKPOINT

Updating Root Component

Once we have everything reorganized I feel comfortable updating the root component to have the application shell we actually need. Since we don’t have any need for navigating around our app yet I’m not going to add many navigation elements, however I will be setting up routing within our root component so that adding future features is quick and easy.

Update the ./app/root/root.component.spec.ts to expect a router outlet to exist in the component:

import { TestBed, async } from '@angular/core/testing';

import { RootComponent } from './root.component';

describe('RootComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        RootComponent
      ],
    }).compileComponents();
  }));

  it('should have router outlet', async(() => {
    const root = TestBed.createComponent(RootComponent);

    expect(root.nativeElement.querySelectorAll('router-outlet').length).toBe(1);
  }))
});

Notice that I’ve removed the existing content of the spec as there is no need for all of that. I also prefer to test the resulting html as much as possible since changes in the component are generally expected to change the html.

Okay lets go ahead and implement our RootComponent given our failing unit test:

# root.component.ts
import { Component } from '@angular/core';

@Component({
 selector: 'app-root',
 templateUrl: './root.component.html',
 styleUrls: ['./root.component.css']
})
export class RootComponent {
}

# root.component.html
<router-outlet></router-outlet>

We would expect this to pass our test as there is a “router-outlet” element in our html. However we end up with an error such as below:

RootComponent should have router outlet FAILED
 'router-outlet' is not a known element:
 1. If 'router-outlet' is an Angular component, then verify that it is part of this module.
 2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<router-outlet></router-outlet>
 "): ng:///DynamicTestModule/RootComponent.html@0:0
 Error: Template parse errors:
 at syntaxError C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:485:22)
 at TemplateParser.webpackJsonp.../../../compiler/esm5/compiler.js.TemplateParser.parse C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:24661:1)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._parseTemplate C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34601:1)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._compileTemplate C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34576:1)
 at http://localhost:9876/_karma_webpack_/webpack:/C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34477:48
 at Set.forEach (<anonymous>)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._compileComponents C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34477:1)
 at http://localhost:9876/_karma_webpack_/webpack:/C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34365:1
 at Object.then C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:474:33)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._compileModuleAndAllComponents C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34363:1)
 'router-outlet' is not a known element:
 1. If 'router-outlet' is an Angular component, then verify that it is part of this module.
 2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<router-outlet></router-outlet>
 "): ng:///DynamicTestModule/RootComponent.html@0:0
 Error: Template parse errors:
 at syntaxError C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:485:22)
 at TemplateParser.webpackJsonp.../../../compiler/esm5/compiler.js.TemplateParser.parse C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:24661:1)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._parseTemplate C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34601:1)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._compileTemplate C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34576:1)
 at http://localhost:9876/_karma_webpack_/webpack:/C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34477:48
 at Set.forEach (<anonymous>)
 at JitCompiler.webpackJsonp.../../../compiler/esm5/compiler.js.JitCompiler._compileComponents C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34477:1)
 at http://localhost:9876/_karma_webpack_/webpack:/C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:34365:1
 at Object.then C:/dev/code/boot-xp/front-end-madness/angular-app/node_modules/@angular/compiler/esm5/compiler.js:474:33)

This error looks pretty bad, but ultimately it is mostly stack trace information. The important part is:

'router-outlet' is not a known element:
 1. If 'router-outlet' is an Angular component, then verify that it is part of this module.
 2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<router-outlet></router-outlet>

Basically this means Angular doesn’t know what to do with the router-outlet element. At this point we have two choices

  1. Use the RouterTestingModule in our test so that the router-outlet component is known to Angular
  2. Tell Angular to ignore unknown custom elements using the ‘CUSTOM_ELEMENTS_SCHEMA’.

I prefer to use option #1 as often as possible. As the components that I’m using are normally my own components or part of an external library and I want to ensure they behave the way I expect them to. In light of this we will add the RouterTestingModule to our test like so:

import { TestBed, async } from '@angular/core/testing';

import { RootComponent } from './root.component';
import {RouterTestingModule} from "@angular/router/testing";

describe('RootComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([])
      ],
      declarations: [
        RootComponent
      ],
    }).compileComponents();
  }));

  it('should have router outlet', async(() => {
    const root = TestBed.createComponent(RootComponent);

    expect(root.nativeElement.querySelectorAll('router-outlet').length).toBe(1);
  }))
});

The updated spec above will now pass because we added the following code:

import {RouterTestingModule} from "@angular/router/testing";
...
describe('RootComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([])
      ],
...
    }).compileComponents();
  }));
...
});

Our tests now pass with a message such as:

Chrome 63.0.3239 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (0.078 secs / 0.071 secs)

CODE CHECKPOINT

Add Customers Module

Finally we are at a point to start adding the customers module. To do this we can return to the angular cli to generate our module and folder for us:

ng generate module customers --routing

This command will generate a .\customers\customers.module.ts and a .\customers\customers-routing.module.ts. The contents of both are:

# customers.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { CustomersRoutingModule } from './customers-routing.module';

@NgModule({
  imports: [
    CommonModule,
    CustomersRoutingModule
  ],
  declarations: []
})
export class CustomersModule { }

# customers-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class CustomersRoutingModule { }

The routes are currently empty for the customers module, but this is a nice way to get started creating a module.

Now we need to return to our root.module.ts and add routing to the module so that it can define how to get to the customers module.

Let’s add a routing module to accommodate our root.module.ts similar to how the customers module is setup.

# root-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
 { path: 'customers', loadChildren: 'app/customers/customers.module#CustomersModule'}
];

@NgModule({
 imports: [RouterModule.forChild(routes)],
 exports: [RouterModule]
})
export class RootRoutingModule {}

Now we need to add our RootRoutingModule to our RootModule imports:

@NgModule({
...
  imports: [
    BrowserModule,
    RootRoutingModule
  ],
...
})
export class RootModule { }

CODE CHECKPOINT

Add CustomersRoot Component

Now that we have an empty module we can start adding functionality to our customers module. To start we’ll use the angular cli:

ng generate component customers/components/CustomersRoot --module customers/customers.module.ts --spec

This will generate the component’s typescript, html, css, and spec. Now we can start writing tests against our CustomersRootComponent. Let’s add a test that defines how to get the list of customers:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { CustomersRootComponent } from './customers-root.component';
import {HttpClientTestingModule, HttpTestingController} from "@angular/common/http/testing";

describe('CustomersRootComponent', () => {
  let httpTestingController: HttpTestingController;
  let component: CustomersRootComponent;
  let fixture: ComponentFixture<CustomersRootComponent>;

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      declarations: [ CustomersRootComponent ],
      imports: [HttpClientTestingModule]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    httpTestingController = TestBed.get(HttpTestingController);

    fixture = TestBed.createComponent(CustomersRootComponent);
    component = fixture.componentInstance;
  });

  it('should get customers from api', () => {
    fixture.detectChanges();
    const req = httpTestingController.expectOne('http://localhost:5000/api/customers');
    expect(req.request.method).toBe('GET');
    expect(req.flush([]));
  });

  afterEach(() => {
    httpTestingController.verify();
  })
});

This test should fail with a message similar to:

Error: Expected one matching request for criteria "Match URL: http://localhost:5000/api/customers", found none.

Now we can write the code required to pass this test:

import { Component, OnInit } from '@angular/core';
import {HttpClient} from "@angular/common/http";

@Component({
  selector: 'app-customers-root',
  templateUrl: './customers-root.component.html',
  styleUrls: ['./customers-root.component.css']
})
export class CustomersRootComponent implements OnInit {

  constructor(private httpClient: HttpClient) { }

  ngOnInit() {
    this.httpClient.get('http://localhost:5000/api/customers');
  }

}

Okay this should work right? Let’s check our test results:

Error: Expected one matching request for criteria "Match URL: http://localhost:5000/api/customers", found none.

WHAT?!?!?! I made the http call its right there what happened? One of the things to remember about Angular is it utilizes RxJS heavily including here. The HttpClient.get method returns us an Observable. This means that unless we have something subscribed to the returned observable the actual http call never happens. We need to update the test to have the following:

..
export class CustomersRootComponent implements OnInit {
...
  ngOnInit() {
    this.httpClient.get('http://localhost:5000/api/customers')
      .subscribe(() => {});
  }

}

Okay do our tests work now?

Chrome 63.0.3239 (Windows 10 0.0.0): Executed 2 of 2 SUCCESS (0.122 secs / 0.116 secs)

Oh good they work now.

CODE CHECKPOINT

Why didn’t I use a service for the http call?

The reason for using HttpClient directly here is that its the simplest thing that could possibly work. Until I have a reason to extract a service for getting customers there is no harm in using HttpClient directly.

What do we do with our retrieved customers?

We will return to our tests to define how we want our component to work:

...
describe('CustomersRootComponent', () => {
  ...
  it('should show customers list', async(() => {
    fixture.detectChanges();

    const req = httpTestingController.expectOne('http://localhost:5000/api/customers');
    req.flush({items: [{}, {}] });

    fixture.detectChanges(); // IMPORTANT
    fixture.whenStable().then(() => {
      expect(fixture.nativeElement.querySelector('.customers-list').length).toBe(1);
      expect(fixture.nativeElement.querySelector('.customer-list-item').length).toBe(2);
    })
  }))
  ...
});

Notice this test is very similar to our end-to-end test. The difference here is we are expecting there to be customers in our list instead of the list being empty. Let’s get this test to pass:

...
import {Customer} from "../../../../../../backend/src/api/customers/customer";
import {ResultList} from "../../../../../../backend/src/api/general/models/result-list";

@Component({
  ...
})
export class CustomersRootComponent implements OnInit {
  customers: Customer[];
...
  ngOnInit() {
    this.httpClient.get<ResultList<Customer>>('http://localhost:5000/api/customers')
      .subscribe(result => this.customers = result.items);
  }
}

The above code only fills in half the picture. We need to go to the customers-root.component.html to finish the rest of the required functionality.

<mat-list class="customers-list">
  <mat-list-item class="customer-list-item" *ngFor="let customer of customers">

  </mat-list-item>
</mat-list>

Notice I’m using mat-list and mat-list-item components. These come from @angular/material. Are the tests passing?

Chrome 63.0.3239 (Windows 10 0.0.0) CustomersRootComponent should show customers list FAILED
 'mat-list-item' is not a known element:
 1. If 'mat-list-item' is an Angular component, then verify that it is part of this module.
 2. If 'mat-list-item' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppre
ss this message. ("<mat-list class="customers-list">

Nope tests still aren’t working. However, the error message should look familiar. This is the same kind of issue we saw when adding router-outlet to our RootComponent. We need to update our testing module:

...
import {MatListModule} from "@angular/material";

describe('CustomersRootComponent', () => {
  ...
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ CustomersRootComponent ],
      imports: [
        HttpClientTestingModule,
        MatListModule
      ]
    })
    .compileComponents();
  }));

  ...
});

Now our tests should be passing:

Chrome 63.0.3239 (Windows 10 0.0.0): Executed 3 of 3 SUCCESS (0.23 secs / 0.212 secs)

Great now we have a working CustomersRootComponent.

CODE CHECKPOINT

Add CustomersRoot To Customers Routing

Now if you were to return to our end-to-end test you would likely see an error like this:

1) Customers should show empty customers
 - Failed: Angular could not be found on the page http://localhost:49152/customers.If this is not an Angular application, you may need
to turn off waiting for Angular.
 Please see 
 https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular-on-page-load

This tells us we are missing something. I believe what we are missing is a route pointing to our CustomersRootComponent. Lets go to our CustomersRoutingModule to add a route:

...
import {CustomersRootComponent} from "./components/customers-root/customers-root.component";

const routes: Routes = [
  { path: '', component: CustomersRootComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class CustomersRoutingModule { }

This solves part of the problem however we have an issue in our RootRoutingModule:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: 'customers', loadChildren: 'app/customers/customers.module#CustomersModule'}
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class RootRoutingModule {}

I forgot to change the RouterModule.forChild(routes) call to RouterModule.forRoot(routes). Okay now with that change everything should be working right?

1) Customers should show empty customers
 - Expected false to be true, 'Customers list is not present on page'.

Nope, but that error looks better than before. Let’s try running our app to see if there are any unseen console errors:

npm start

Once the cli has finished building our app we can go to http://localhost:4200/customers. If you are like me you see the following error in the console of the browser:

Uncaught (in promise): Error: Template parse errors:
'mat-list-item' is not a known element:
1. If 'mat-list-item' is an Angular component, then verify that it is part of this module.
2. If 'mat-list-item' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<mat-list class="customers-list">

Aha that again looks familiar. We changed our testing module for CustomersRootComponent however we didn’t add the MatListModule to our CustomersModule. We can do that fairly easily:

...
import {MatListModule} from "@angular/material";
...
@NgModule({
  imports: [
    CommonModule,
    MatListModule,
    CustomersRoutingModule
  ],
...
})
export class CustomersModule { }

Any luck? Nope still have an error in the browser’s console:

ERROR Error: Uncaught (in promise): Error: StaticInjectorError(RootModule)[CustomersRootComponent -> HttpClient]:

This is a bit different from the previous error, but we can use a similar fix. The issue is that Angular’s dependency injection doesn’t know what an HttpClient is so it can’t inject that into our CustomersRootComponent. We need to add the following to our CustomersModule:

...
import {HttpClientModule} from "@angular/common/http";
...
@NgModule({
  imports: [
    CommonModule,
    MatListModule,
    HttpClientModule,
    CustomersRoutingModule
  ],
...
})
export class CustomersModule { }

Okay now I only see one error in the browser’s console complaining about the http call failing.

We will get to that next time. Let’s see if our end-to-end test works now:

1) Customers should show empty customers
 - Expected false to be true, 'Customers list is not present on page'.

Huh…. That seems odd everything works in the browser? Take a quick look at the CustomersPage object in our end-to-end test:

import { browser, element, by } from 'protractor';

export class CustomersPage {
  navigateTo() {
    browser.get('/customers');
  }

  getCustomersList() {
    return element(by.className('customer-list'));
    return element(by.className('customers-list'));
  }

  getCustomers() {
    return element.all(by.className('customer-list-item'));
  }
}

Looks like I had the wrong class name in there. Try it now using the correct class name:

Customers
 √ should show empty customers

Finally we have a working end-to-end test.

CODE CHECKPOINT

Wrapping Up

Up to this point we have our first couple of modules, components, tests, and one end-to-end test. One thing we know is broken is the http call to get clients from the api? We’ll look at that next time. I’ve setup a simple back-end that we need to get started as part of running our app and end-to-end tests.

Front-end Madness: Angular – Customers

Front-end Madness: Angular – Getting Started

Given the number of front-end frameworks to choose from I wanted to start looking at the similarities and differences between frameworks. In light of this I decided to start building the same application in each framework that I find interesting. I’ve decided to start with Angular as I’m more comfortable with Angular than most other frameworks.

Source Code: GitHub Repository

Start with the CLI

Anyone getting started with Angular should get their application started using the Angular CLI. This is an npm package that can be installed using the command:

npm install @angular/cli --global

Once the Angular CLI is installed you can start your project using:

ng new {app name}

This will give you a starting point that looks similar to the application found here.

The cli can be used with different flags to configure how to generate your new project. A few that I find handy are:

ng new {app name} --directory {target directory} # Specify directory for app
ng new {app name} --source-dir {app source directory} # Directory to place app code defaults to src
ng new {app name} --prefix {prefix} # Prefix to use for components when generating components using the cli

What’s in the box?

Now that we have a solid starting point let’s look at what has been generated.

The key files for the CLI and build process are:

  • ./.angular-cli.json – Defines how the cli will build, lint, bundle, and generate your application.
  • ./tsconfig.json – Defines how TypeScript will transpile your application.
  • ./src – Directory containing your application source code.
    • This is the directory name you are changing if you use the –source-dir flag.
  • ./e2e – Directory containing End-to-End protractor tests.
    • This can be skipped using the –skip-tests flag
  • ./src/main.ts – Entry point of your application.
  • ./src/polyfills.ts – Provides various polyfills.
    • The Angular team has been nice enough to leave lots of comments in this code indicating which polyfills are needed for various browsers
  • ./src/app/app.module.ts – Root application module
  • ./src/app/app.component.ts – Root application component
    • This is the root component in your application.
    • For more information on Angular Components check out the angular docs.
  • ./src/test.ts – Entry point for unit tests
    • This interestingly won’t be skipped when using –skip-tests flag

There is a lot of other code generated for you, but these ones are the big hitters you need to know about to get started.

Running Your Application

You can start your application using one of the following commands:

ng serve # uses the angular cli to serve your application
npm start # uses npm to run the ng serve command

I prefer to use the npm commands as instead of using the cli commands.

You can run your unit tests using one of the following commands:

ng test 
npm test

You can run your end-to-end tests using one of the following commands:

ng e2e
npm run e2e

When developing my angular applications I generally have a command prompt running tests and serving my application so that I can quickly get feedback on my progress. Fortunately, the angular cli sets up npm start and npm test to watch for file changes.

 

 

Front-end Madness: Angular – Getting Started

Developer Learning

Recently, I’ve been giving more thought to the idea of how can we improve the training/development of developers. My main curiosity has been revolving around preparing new developers for what they will encounter in the “real world.” I keep bouncing back and forth between thinking developer boot camps are the way to go and thinking the traditional four year degree in CS, CIS/MIS, SE, etc. is the best way. However, I honestly think those two options are just the extreme ends of a spectrum of options.

Developer Boot Camps

I’ve had the privilege of working with a few developers that have come out of developer boot camps. What I have gathered from this experience is that developer bootcamps focus on getting new developers up-to-speed on one technology stack. For example this would mean taking a person from knowing nothing about development to being comfortable working in a Ruby on Rails application. In theory this sounds fantastic and from my experience it works pretty well. The caveat here is that theory and the like are go unknown to boot camp graduates, unless graduates pursue that kind of information on their own. (Disclaimer: I’m not an expert on boot camps and their curriculum) To be clear when I say theory I’m talking about concepts like encapsulation, polymorphism, etc.

Looking at boot camps like this they appear to live on the extreme side of just get used to the tools, one language, and a small set of technologies. The developer is then left with the knowledge of the mechanics, but I feel has missed quite a bit of the why. The “why” is extremely important when it comes to deciding how to break down a system, how to split up work, how to increase application flexibility, etc. Essentially without having a deep understanding of “why” a developer may not have the knowledge to make good design decisions. The design decisions I’m talking about here aren’t overarching application architecture, but decisions like: should I use inheritance here? should I break this class/module/function down? where does this functionality belong? These are questions every developer has to answer multiple times a day. Making good decisions at a micro level contributes to the overall maintainability of the system.

Now let me be clear here I think developer boot camps offer a tremendous way to enter the field of software development at a much more reasonable cost than a traditional four year degree.

Traditional Four Year Degree (CS, MIS/CIS, SE)

I can’t speak for everyone’s four year degree, but I know that when I entered the work force after college I was slapped in the face with here’s an application we need you to work on. I was a 22 year old kid just out of school and now working on a system that would be handling billions of dollars worth of transactions and inventory. I wasn’t a lead or anyone special I was just another developer on the team. I did however feel unprepared for the development that was needed. For all my four year degree taught me I hadn’t actually built a full blown application of any real size. The largest application I had built in college was probably ~2,000 lines of code max, I feel that’s a gross over estimation. My four year degree had focused almost entirely on theory subjects encapsulation, polymorphism, algorithms, etc. This kind of learning prepared me for thinking through problems and understanding what terms meant, but I had no idea how to apply most of those theories in practice.

To mean it is the putting theories to practice that is completely lacking from at least my curriculum, and from what I’ve heard, other curriculum as well. My four year degree didn’t teach me how to build applications. The closest thing to an application I built was a very simple php application that equated to about 500 lines total. The one application was the only application I had built that used an actual database. In the “real world” every application I’ve worked on has used at least one if not a couple different databases. Four year degrees seem to miss the mark when it comes to teaching students about the mechanics of building applications. Four year degrees, to me, represent the theory extreme end of the spectrum.

Finding the Middle Ground

Thinking of developer boot camps and traditional CS, MIS/CIS, SE degrees as extreme opposites pushes me to want to find a middle ground. There must be some way to combine the two ideas into a more effective approach. I can think of a few options that could be a middle ground:

  • In a four year degree have students select an application they will build throughout their degree?
  • In a boot camp could students spend a week or so pairing with experienced developers?
  • Should four year programs begin to partner with companies to get their students exposure to “real world” applications?
  • Should boot camps spend a week or more looking at code that exhibits a good and bad use of polymorphism, algorithms, etc?

Those are just a few ideas that could help close the gap. There are probably examples of each of these occurring already I’m just unaware of them.

Developer Learning

Deploy Azure Functions App From AppVeyor

At work recently we have been starting to use Azure functions for a fairly small job in our infrastructure. Also at work we use AppVeyor for our CI/CD server. I like AppVeyor and it has served us well thus far. However, since the tooling and idea of Azure Functions is relatively new it was a bit hard to find a good example of a way to deploy an Azure Functions App from AppVeyor. Luckily one blog had a good starting point, however there are things I was unaware of about Azure Functions that were missing from the blog post. One of the biggest ones was how the function.json and host.json files are created.

Function.json

If you are new or haven’t researched the output of building an azure function there are a few things you need to be aware are happening behind the scenes, especially if you want some sort of CI/CD. In a functions app project you define your functions like below:

using System;
...

namespace Your.Function.App
{
    public static class YourFunction(
        // This trigger piece is important.
        [ServiceBusTrigger("{topic}", "{subscription}", AccessRights.Listen, Connection = "{connectionName}")] object message,
        TraceWriter logger)
    {
        // Perform you function logic here
    }
}

The important piece to notice here is the function trigger. In this case the trigger is a service bus message arriving for a specific service bus topic. For a list of triggers available take a look at here. If you are familiar with Azure WebJobs you may think that this is all you need, however that trigger attribute is really just the first piece of the puzzle. The part of the puzzle we can’t see yet is the generated json file this attribute helps create. Here is a sample of the json that would result from the above code.

{
  "generatedBy": "Microsoft.NET.Sdk.Functions-1.0.0.0",
  "configurationSource": "attributes",
  "bindings": [
    {
      "type": "serviceBusTrigger",
      "connection": "{connectionName}",
      "topicName": "{topic}",
      "subscriptionName": "{subscription}",
      "accessRights": "listen",
      "name": "message"
    }
  ],
  "disabled": false,
  "scriptFile": "{relative path to output}\\{Your assembly name}",
  "entryPoint": "Your.Function.App.YourFunction.Run"
}

The big question here is how does this file get generated? You won’t see it in your solution and if you deploy from Visual Studio you won’t even know, unless you look for it, that the file even exists. I didn’t know this and didn’t realize it was needed, however this file is what Azure uses to know how and when to trigger your function so without it you won’t see any functions in your Azure Functions App. I know you won’t see any functions because that’s exactly what happened to me when attempting to duplicate Alastair Christian’s blog post. The issue I was seeing had nothing to do with the content of the blog it was my lack of knowledge of how Azure Functions work.

What were we missing?

The part I missed when attempting to duplicate Alastair’s solution was a small one but extremely important in this context. He was using MSBuild to build his solution and I was using the dotnet cli.

In this case: msbuild.exe Solution.sln != dotnet build

Remember the all important function.json file that gets generated? It turns out that is generated from some of the targets that Visual Studio’s version of msbuild uses that the dotnet cli’s msbuild won’t have available (to be clear there is likely a way to resolve this, but I didn’t dig for it).

How did we fix it?

When using AppVeyor, and likely any other CI server, is fairly straight forward. If you are using the dotnet cli for your existing builds, but want to start using Azure Functions be sure that you change your scripts to use:

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe {solution}.sln

instead of:

dotnet build {solution}.sln

This will ensure that your output generates the function.json appropriately. You could attempt to maintain the function.json yourself, however if the file can be generated from our code I don’t want to worry about it.

Now that we build, how do we deploy?

This part is very straight forward and I was able to use Alastair’s example with one small tweak, I prefer to use appveyor.yml files instead of the AppVeyor interface for configuring our builds (this is a preference and direction we have chosen at work). So as an alternative to the user interface you can add deployment to your AppVeyor build altering the below example:

image: Visual Studio 2017
...
artificats:
- path: '{relative path to functions output}'
  name: '{name of artifact}'
...
deploy:
- provider: WebDeploy
  server: https://{azure site name}.scm.azurewebsites.net/msdeploy.axd
  website: {azure site name}
  username: {user name that has deployment access}
  password:
    secure: {secured password}

And that’s it now you have an automated deployment of Azure Functions from AppVeyor. I hope that helps you or someone you know.

Deploy Azure Functions App From AppVeyor