It's time to do some Angular! This dojo contains a bunch of things that Matt likes about Angular, and only scratches the surface of what it is capable of. If you're interested in learning more, the best place to start is the official docs: https://angular.io/docs/ts/latest/
- Install node & npm. You can use yarn if you prefer!
- Install git
- Install angular-cli, by running
npm install -g @angular/cli
in a command window - Install gulp, by running
npm install -g gulp
in a command window - Install vs2017
- Install vscode, and this auto import extension
Follow these steps to create a blank project, and run it locally. Keep it running for the rest of this dojo:
- Open a new git bash window (or cmd if you prefer)
- Change directory to where you'd like to create your new project folder (i.e.
cd /c/projects
), and run the following commands:ng new angular-dojo-frontend
cd angular-dojo-frontend
code .
ng serve
You should now be able to browse to your shiny new Angular site, using the port shown in your command window. (i.e. http://localhost:4200). You should also have your new project open in vscode, ready for editing.
For this dojo, we'll use Semantic UI as our CSS framework. First we need to install Semantic UI, and build the CSS and JS:
- Open another git bash window (or cmd if you prefer). We'll use two windows, so you can leave your other one serving your app.
- Change directory to your project route (i.e.
cd /c/projects/angular-dojo-frontend
), and run the following commands:npm install semantic-ui jquery --save
(Note: due to a quirk in the semantic setup, you may need to run this in cmd rather than git bash. Install using all the defaults)cd semantic
gulp build
Now we need to reference the compiled semantic CSS and JS, along with jquery. Crack open vscode, and edit angular-cli.json
:
"apps": [{
...
"styles": [
"styles.css",
"../semantic/dist/semantic.min.css"
],
"scripts": [
"../node_modules/jquery/dist/jquery.min.js",
"../semantic/dist/semantic.min.js"
],
...
}]
You'll need to kill and re-run ng serve
in the first command window, so that it picks up the updated Angular config. Now let's see if it's working, by editing app.component.html
:
- Wrap the contents of the component in a
div
, with classui container segment
- Edit the
h1
, so it has classui header
- Add a
p
below theh1
, with a bit of content
If you've left the Angular site running, you should see all of your changes show up in real time. For bonus points - read up on view encapsulation and shadow DOM.
Let's play around with some Angular binding, by editing app.component.html
:
- Add
<input [(ngModel)]="title">
to the component - Wrap the input in a
<div>
with classui input
Note that this is a two way binding. Notice how the value displayed in the <h1>
(which is using a one way binding) automatically changes in real time, as you edit the input field.
Create a new navigation component using angular-cli. Open your second command window (the one not serving the app):
- Change directory back to your project route (i.e.
cd /c/projects/angular-dojo-frontend
) ng g component navigation
Notice how it creates the scaffolding for our new component, and also edits our app definition in app.module.ts
. Now let's add it into our app:
- Edit
/navigation/navigation.component.html
. Add a semantic menu, something like:
<div class="ui top menu">
<a class="item">Home</a>
<a class="item">About</a>
</div>
- Add
<app-navigation></app-navigation>
to the top ofapp.component.html
.
Let's get some routing going:
- Create two new components:
- home
- about
- Prettify them, by wrapping the contents of their HTML in a
<div class="ui container segment">
- Add a
<router-outlet></router-outlet>
to the bottom ofapp.component.html
. - Add an
app.routing.ts
file to the app root, with contents like:
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
]
export const routing = RouterModule.forRoot(routes);
- Edit
app.module.ts
, and addRouterModule
androuting
to theimports
list - Edit
navigation.component.html
, and add attributes[routerLink]="['']"
and[routerLink]="['about']"
to their respective<a>
s - Bask in your own glory. For bonus points - look into how Angular handles child routes
Let's create a service, so that we can put some data on the screen:
ng g service data-access
- Edit the new service, and give it a method that returns some values. Something like:
getValues(): string[] {
return ["1", "2"];
}
- Inject the new service into
about.component.ts
. Don't instantiate it! (Hint:constructor(private dataAccessService: DataAccessService)
) - Provide the new service in
about.component.ts
. Hint:providers: [DataAccessService]
- Call the new
getValues()
method when theabout
component initiates (the lifecycle hook should already be there, thanks to angular-cli). Assign the return value to a local variable (sayvalues
) - Edit
about.component.html
, to actually display the values. Here we can use some Angular syntax, like:
<ul>
<li *ngFor="let value of values">
{{value}}
</li>
</ul>
To simulate real-life, let's make our service asynchronous, and introduce an artificial wait-time into our service. Start by editing data-access.service.ts
:
- Change the signature of our
getValues()
method, so that it returns aObservable<string[]>
- Change the return value to
Observable.of(["1", "2"])
We'll now have a compile error. Fix this by editing about.component.ts
:
- In
ngOnInit
, change the line that calls the service to:this.dataAccessService.getValues().subscribe(result => this.values = result);
We're not quite there, as our service is still returning data instantaneously. Let's slow it down, by editing the return value of getValues()
in data-access.service.ts
again:
return new Observable(subscriber => {
setTimeout(() => subscriber.next(["1", "2"]), 1000);
});
Now test it out, and observe how the about component takes a second to display the values on the screen, every time it is initiated. We can take this one step further, and display something on screen whilst we're waiting. Let's do that now:
- In
about.component.ts
, add a new local variable,isLoading: boolean
- Also in
about.component.ts
, insidengOnInit
, start by settingthis.isLoading = true
. When the values are returned from the service, setthis.isLoading = false
- In
about.component.html
, display something when loading:
<div *ngIf="isLoading" class="ui active centered inline text loader">
Loading...
</div>
In real life you might not want to instantly load the page and show some holding content whilst waiting for data. You may want to wait until all the data is present before activating the route. This can be achieved with route resolvers. They're not covered in this dojo, but are fairly straightforward. You can read more at thoughtram.
This repository contains a pre-built dotnet core webapi project. You could clone this repo, then open the solution within ./api/src/
in Visual Studio, and run the webapi project. Or, if you're lazy, you can use the pre-existing backend API in Azure, at: http://angular-dojo.azurewebsites.net
With the webapi project running, you should be able to browse to the Swagger endpoints, thanks to NSwag:
- Swagger UI: http://angular-dojo.azurewebsites.net/swagger (or http://localhost:4201/swagger if you're running locally)
- Swagger JSON: http://angular-dojo.azurewebsites.net/swagger/v1/swagger.json (or http://localhost:4201/swagger/v1/swagger.json if you're running locally)
Let's use NSwag to generate a TypeScript client for us. Back in our dojo front-end project:
- Install NSwag 8.0.0*:
npm install nswag@8.0.0 -g
- Generate the TS client:
nswag swagger2tsclient /input:http://angular-dojo.azurewebsites.net/swagger/v1/swagger.json /output:./src/api/apiclient.ts /template:angular2
(you might want to add this command as a script insidepackage.json
) - Edit
about.component.ts
:- Add import:
import * as apiClient from '../../api/apiclient';
- Add provider:
providers: [apiClient.ValuesClient]
- Change injected service:
valuesClient: apiClient.ValuesClient
- Change method call:
this.valuesClient.getAll().subscribe(result => ...
- Add import:
Check that the data shows up on the screen.
*For some reason the latest version of NSwag wasn't working properly at the time of writing, but 8.0.0 was. Feel free to try the latest version if you want. Note that the template name is simply angular
, not angular2
when using the latest version.
Let's create a quick form:
- Create a new component called
form
- Add a new route for this new component, in
app.routing.ts
- Add a new
routerLink
into our nav component, so we can navigate to it - Edit the new
form.component.ts
:- Add a local variable
myInput: string
- Create a blank method
submit()
. We'll use this later - Add
import * as apiClient from '../../api/apiclient';
- Inject
apiClient.ValuesClient
into the constructor, as before
- Add a local variable
- Move the line that provides
apiClient.ValuesClient
. This is currently inabout.component.ts
. We can move this up a layer, so that it is provided inapp.module.ts
, to save us having to duplicate any code - Edit the new
form.component.html
:- Replace the default contents with a
<form>
with classui form
. Also add(ngSubmit)="submit()"
to this<form>
. This hooks the submit method into thesubmit()
method we defined earlier on the component - In the form, add an
<input>
of typetext
, with[(ngModel)]
set to the local variablemyInput
created earlier. You'll also need to set thename
to something, otherwise Angular will moan - Wrap this
<input>
in a<div>
with classfield
- Add a
<button>
of typesubmit
, with classui button green
- Wrap the contents of the
<form>
in a<div>
with classui container segment
- Replace the default contents with a
Now, because the form input is bound to our local variable myInput
, when we submit the form we can access the user's input through this variable. This lets us easily post to the api from inside the component. Edit form.component.ts
:
- Inside
submit()
, add a line likethis.valuesClient.post(this.myInput).subscribe();
If you put a breakpoint in Visual Studio, inside the post method of ValuesController
, you should see that your entered value has arrived on the server. Cool!
However, it could be cooler by being a bit more responsive. We can change that pretty easily:
- Add a new local variable to
form.component.ts
:isSubmitting: boolean;
- In
submit()
, start by settingthis.isSubmitting = true
. When the response comes back from the api, set this back tothis.isSubmitting = false
- In
form.component.html
, add[disabled]="isSubmitting"
to both the<input>
and<button>
- Add
[class.loading]="isSubmitting"
to the<button>
Try submitting the form again. Cooler!
Bonus points: You can move the [disabled]="isSubmitting"
attribute onto a <fieldset>
wrapping all the form elements, rather than replicating it on each element.
Let's check out Angular's pipes:
- Edit
home.component.ts
- add a local variableexampleDate: Date = new Date();
- Edit
home.component.html
- displayexampleDate
, using{{exampleDate}}
Notice how the full Date object is displayed. Let's change that with a pipe:
- Change the binding to
{{ exampleDate | date }}
Ooh shiny. You can of course create your own pipes, and you can parameterise them too.
Let's check out Angular's attribute directives. Create a new directive using angular-cli:
ng g directive highlight
Now edit highlight.directive.ts
:
- Inject
private el: ElementRef
into the constructor - Make the directive implement
OnInit
, and add thengOnInit
method. Edit it to something like:
ngOnInit() {
this.el.nativeElement.classList.add("ui", "segment", "yellow");
}
Now let's actually use our directive, by adding it as an attribute to a div:
<div appHighlight>
Hooray! We can also add @Input
properties onto directives, for further customisability. Edit highlight.directive.ts
:
- Add an input property:
@Input() highlightColor: string;
- Use this new property, instead of hard-coding
"yellow"
, inngOnInit()
Now we can use our directive along with its input property:
<div appHighlight highlightColor="yellow">
Bonus points: make the syntax more compact, so that we can just write <div appHighlight="yellow">
. You can do this by matching the name of the @Input
property to the directive's name. To avoid confusion inside the directive, you can use an input alias.
You may have noticed that angular-cli has been generating *.spec.ts
files whenever we create anything. And in fact, they come with a few built-in tests, ready to go. To run them, use:
ng test
Woops - they fail! We need to do some quick fixes to accommodate all the code we've already written:
- In
about.component.spec.ts
, we need to import Angular's HTTP module. Addimports: [HttpModule]
to theTestBed.configureTestingModule
method - Also in
about.component.spect.ts
, we need to provide the ValuesClient. Addproviders: [apiClient.ValuesClient]
(andimport * as apiClient from '../../api/apiclient';
) - In
app.component.spec.ts
, we need to tell Angular about our custom components. Addschemas: [CUSTOM_ELEMENTS_SCHEMA]
(and importCUSTOM_ELEMENTS_SCHEMA
from@angular/core
) - Also in
app.component.spec.ts
, we need to import Angular's forms module. Addimports: [FormsModule]
- In
navigation.component.spec.ts
, we need to import Angular's router testing module. Addimports: [RouterTestingModule]
(and importRouterTestingModule
from@angular/router/testing
) - In
form.component.spec.ts
, we need to import both Angular's HTTP module, Angular's forms module, and we need to provide the ValuesClient. Use the same approach as above - Finally, just delete
highlight.directive.spec.ts
, as we're not going to test it in this dojo
And now, after running ng test
again, the tests should all pass.. hooray!
Let's mock our apiClient.ValuesClient
, so we can test that the about component is populating itself correctly. Edit about.component.spec.ts
:
- Create a class,
MockValuesClient
that extendsapiClient.ValuesClient
. OverridegetAll()
, by defining a mock method that returnsObservable.of(["value3"]);
- Configure the test bed to use an instance of
MockValuesClient
wheneverapiClient.ValuesClient
is needed. Here's where providers come in handy, as we can change out the providers line to:providers: [{ provide: apiClient.ValuesClient, useClass: MockValuesClient }]
- Create a new test method to check that the values from the api are populating the component's local variable. Something like
expect(component.values).toContain("value3");
The test should pass. Bonus points: take it one step further, and test that the value from the mock client is displayed on the screen.
Angular CLI runs on webpack. When running the local webpack dev server (i.e. ng serve
), the best way to debug your code is through Chrome:
- Open the dev console (F12)
- Go to Sources
- Ctrl + P -> start typing the name of the .ts file you want to debug
- Add a breakpoint
To build the solution ready for deployment, just run ng build
(or ng build --prod
if you want to remove source maps, and run uglify). You can host the /dist
folder on any web server.