powered by Para

Building a full stack application from scratch with Angular

UPDATE: This article and its associated code have been updated for Angular 7.x.

In this tutorial we’re going to build a simple single-page application with Angular 2+. This is intended for developers unfamiliar with 2+ or having some experience with AngularJS. First of all, I got Visual Studio Code installed on my machine and it’s running on Linux. I chose VS Code because we’ll be working with TypeScript mostly and it has great support for it, but you can code in your favourite IDE as well. Next, I’ve decided to save some time and clone the excellent Angular 2 starter kit by Minko Gechev called ‘angular-seed’. You’ll also need to have Git, Node.js and npm installed.

Step 0 (frontend)

1
2
3
4
5
6
git clone --depth 1 https://github.com/mgechev/angular-seed.git angular2-para
cd angular2-para
# install the project's dependencies
npm install
# watches your files and uses livereload by default
npm start

Next - the backend. Here, I could write a simple backend in Node.js and Express but I’m lazy so I chose not to. Instead, I’m going to use Para for my backend and I’m not going to write any code on the server. If you are new to Para, it’s a general-purpose backend framework/server written in Java. It will save me a lot of time and effort because it has a nice JSON API for our app to connect to. To run the server you’re going to need a Java runtime.

Step 0 (backend)

  • Get Java
  • Get Para
  • Start the server in a separate terminal:
1
2
# run Para
java -jar para-x.y.z.war

Now, check if Para is running - open your browser and go to http://localhost:8080/v1. You should see a response like this:

1
2
3
4
{
"info" : "Para - a backend for busy developers.",
"version" : "x.y.z"
}

We haven’t got access keys to the server yet, so let’s go ahead and do that, open:

1
http://localhost:8080/v1/_setup

Save the credentials to a file, we’ll need them later to access the backend API.

Step 1 - API access

Let’s create an app for storing recipes - a recipe manager. Our goal will be to build just the basic CRUD functionality, without adding extra features like authentication and login pages. By default the backend is secured and only signed requests are allowed, but for the purpose of this tutorial we’re going to add a new permission to allow all requests to just one specific resource - /v1/recipes.

Go to console.paraio.org and enter the credentials that you saved in the beginning. Also click the cog icon to edit the API endpoint and set it to http://localhost:8080. Click ‘Connect’.

Next, go to ‘App’ on the left and edit the root app called para. You’ll see a section for resource permissions and there you will write a simple permission definition in JSON:

1
2
3
4
5
{
"*": {
"recipes": ["*", "?"]
}
}

This defines a single permission that allows * - everyone to access /v1/recipes using a list of allowed methods, in this case * - all HTTP methods and ? - anonymous access is allowed. Thus, we’re essentially making this resource publicly available. Click ‘Save Changes’.

Step 2 - CRUD recipes

Now let’s go back to our frontend and edit the ‘Home’ component under src/client/app/home. We want to edit the HTML code a little bit in home.component.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<ul>
<li *ngFor="let recipe of recipesList; let i = index" class="recipe-box">
<div [hidden]="editedRecipes.get(recipe.id) || (!recipe.id && createMode)">
<h3>{{recipe.name}}</h3>
<hr>
<div [innerHTML]="md2html(recipe.text)"></div>
<br>
<button href="#" (click)="editRecipe(recipe)">edit</button> &nbsp;
<a href="#" (click)="removeRecipe(recipe.id)" class="red right">remove</a>
</div>
<div [hidden]="(recipe.id || !createMode) && !editedRecipes.get(recipe.id)">
<form (submit)="addRecipe(recipe)">
<div>
<input [(ngModel)]="recipe.name" placeholder="Title" [name]="'name' + i">
</div>
<br>
<div>
<textarea [(ngModel)]="recipe.text" rows="10" cols="33" placeholder="Recipe" [name]="'text' + i"></textarea>
</div>
<button type="submit">
<span *ngIf="createMode">Add</span>
<span *ngIf="!createMode">Save</span>
</button>
&nbsp;
<a href="#" (click)="closeForm(recipe.id)">Close</a>
</form>
</div>
</li>
</ul>

I’ve added the “Add” button which shows the form where we can write a recipe (controlled by newRecipeForm()), a textarea, and a close button. Notice how the text value of the “Add” button changes to “Save” when we’re in edit mode. Coming from AngularJS, you’ll notice the weird [(ngModel)] syntax - it’s a two-way binding (single brackets is one-way). Similarly, *ngIf is just shorthand for [ngIf].

Also, I chose to set a new title in the header section in src/client/app/core/toolbar/toolbar.component.html:

1
2
<h1>Recipe Manager <code><small>v{{version}}</small></code></h1>
<div class="more"></div>

Let’s edit the NameListService which is part of the starter project and rename it to RecipesService. You’ll have to rename all occurrences of the class and also rename the folder src/client/app/shared/name-list. In the code for home.component.ts we’ll add a few fields to manage the recipes. The start of that component should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
export class HomeComponent implements OnInit {
recipesList: Array<any>;
createMode = false;
q: string;
editedRecipes: Map<string, boolean>;

constructor(public recipeService: RecipeService) {
this.editedRecipes = new Map<string, boolean>();
this.recipesList = new Array();
}

addRecipe(): boolean { }
}

Now we’re going to focus on that addRecipe() method so let’s implement it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addRecipe(recipe: any) {
this.recipeService.add(recipe.name, recipe.text)
.subscribe((data: any) => {
if (data) {
if (this.createMode) {
const first = this.recipesList.shift();
this.recipesList.unshift(data);
this.recipesList.unshift(first);
} else {
this.recipesList.unshift(data);
}
}
});
this.closeForm(recipe.id);
}

Let’s also add the method for listing recipes listRecipes() and call it upon initialization:

1
2
3
4
5
6
7
8
9
10
ngOnInit() {
this.listRecipes();
}

listRecipes() {
this.recipeService.get()
.subscribe((data: any) => {
this.recipesList = data.items;
});
}

Now we have to modify the RecipeService in recipe.service.ts to allow for another parameter text in the add() method. Let’s also add the code for making the POST request to the backend:

1
2
3
4
5
6
7
8
private appID = 'app:myapp';
private RECIPES_RESOURCE = Config.API + '/v1/recipes';

add(name: string, text: string) {
if (!name || !text) { return of(null); }
const recipe: any = { name: name, text: text };
return this.http.post(this.RECIPES_RESOURCE, JSON.stringify(recipe), this.options);
}

You’ll notice that in home.component.ts, we subscribe to the Observable returned by recipeService.add() and get back the list of recipes when they arrive.

1
2
3
4
5
6
this.recipeService.add(this.newName, this.newRecipe).subscribe((data: any) => {
// response might be null or empty
if (data) {
this.recipesList.unshift(data);
}
});

In home.component.html we loop over the recipesList of all available recipes, and also a box which appears when there are no recipes to show:

1
2
3
4
5
6
7
8
<ul>
<div class="empty-box" *ngIf="recipesList && recipesList.length == 0">
No recipes to show.
</div>
<li *ngFor="let recipe of recipesList" class="recipe-box">
...
</li>
</ul>

Let’s add the styling for .recipe-box and .empty-box later in home.component.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.recipe-box {
display: inline-table;
width: 300px;
margin: 30px 30px 0 -7px;
padding: 20px;
border: 1px solid #106cc8;
}
.empty-box {
height: 200px;
width: 100%;
padding: 1.5em 0 1em 0;
font-size: 3em;
color: #ccc;
border: 3px dashed;
text-align: center;
}

In main.css I’ve also added a few more tweaks to the CSS:

1
2
3
4
5
6
7
8
9
10
11
input, textarea {
border: 1px solid #106cc8;
font-size: 14px;
outline: none;
padding: 8px;
}
button:hover { background-color: #28739e; }
button.small { font-size: 12px; height: 30px; }
.red { color: indianred; }
.right { float: right; }
.center { text-align: center; }

So, we should now we able to add recipes and after we click “Add” the form should be cleared and closed. For this let’s add a couple of methods in home.component.ts - one to initialize the form and one to reset the state of the form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
newRecipeForm() {
if (!this.createMode) {
this.recipesList.unshift({name: '', text: ''});
this.createMode = true;
}
}

closeForm(recipeId: string) {
if (recipeId) {
this.editedRecipes.set(recipeId, false);
} else if (this.createMode) {
this.recipesList.shift();
this.createMode = false;
}
}

The variable recipeId will keep the value of the id when a recipe is being edited. When “Save” is clicked this id is passed to the service and the backend so it won’t create a new object, just update an existing one. We’re issuing these requests and we don’t care about the results because we can update the UI instantly, without having to wait for the request to finish.

1
2
3
4
5
6
7
8
editRecipe(recipe: any) {
this.editedRecipes.set(recipe.id, true);
}

removeRecipe(id: string) {
this.recipeService.remove(id).subscribe();
this.recipesList = this.recipesList.filter((el) => el.id !== id);
}

Let’s also add similar methods in our recipeService for updating and deleting recipes. The methods editRecipe() and removeRecipe() are relatively straightforward - when editing, we switch to edit mode and we show the form, when removing we just filter the array recipesList and we discard the deleted recipe if it matches the id.

1
2
3
4
5
6
7
8
9
10
edit(id: string, name: string, text: string) {
if (!id) { return of(null); }
const recipe: any = { name: name, text: text };
return this.http.patch(this.RECIPES_RESOURCE + '/' + id, JSON.stringify(recipe), this.options);
}

remove(id: string) {
if (!id) { return of(null); }
return this.http.delete(this.RECIPES_RESOURCE + '/' + id, this.options);
}

In home.component.ts we’ll modify the code for addRecipe() to also edit a recipe when recipe.id is set.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addRecipe(recipe: any) {
if (recipe && recipe.id) {
this.recipeService.edit(recipe.id, recipe.name, recipe.text).subscribe();
} else {
this.recipeService.add(recipe.name, recipe.text)
.subscribe((data: any) => {
if (data) {
if (this.createMode) {
const first = this.recipesList.shift();
this.recipesList.unshift(data);
this.recipesList.unshift(first);
} else {
this.recipesList.unshift(data);
}
}
});
}
this.closeForm(recipe.id);
}

We can now add, edit and remove recipes but they aren’t very pretty and the formatting of the text is lost. In the next step we’ll make it possible to write the recipe text in Markdown and then render it in HTML.

Step 3 - Markdown support

First of all, let’s install showdown - a nice JavaScript parser for Markdown:

1
2
npm install showdown --save
npm install @types/showdown --save-dev

Then we import it in home.component.ts:

1
import { Converter } from 'showdown';

Finally we’ll implement a simple method called md2html() which will be used in our template.

1
2
3
md2html(text: string): string {
return new Converter().makeHtml(text || '');
}

In our HTML template we call it like this:

1
<div [innerHTML]="md2html(recipe.text)"></div>

Now we render the text to HTML on the client and this allows us to write beautiful recipes like this:

The final feature left is the recipe search box. We’ll use the built-in full-text search in Para. In recipe.service.ts:

1
2
3
search(q: string) {
return this.http.get(this.RECIPES_RESOURCE + '?q=' + q, this.options);
}

And in home.component.ts:

1
2
3
4
5
6
7
8
search(): boolean {
this.recipeService.search(this.q || '*').subscribe((data: any) => {
if (data.items) {
this.recipesList = data.items;
}
});
return false;
}

Finally, we add the search box in the template below the heading:

1
2
3
4
5
6
<h1>My Recipes &nbsp; <button (click)="newRecipeForm()">Add</button></h1>
<div>
<form (submit)="search()">
<input type="text" [(ngModel)]="q" name="searchText" placeholder="Search">
</form>
</div>

And we’re done! Here’s final result of our Recipe Manager (check out the live demo):

Summary

Learning Angular takes some time as it introduces a lot of architectural changes and new syntax. Writing in TypeScript feels fresh and more like writing in a real statically typed language like C# or Java, rather than a dynamic language like JS. The import syntax was a bit hard for me to get used to, especially with all the different files I had to navigate through. In general, the experience of writing Angular apps is great - the syntax is clean, the app is well structured and the error messages are clear and understandable.

Things we did:

  • wrote a few fancy AJAX calls to our backend API
  • wired a bunch of simple TypeScript code between a component and a service
  • wrote some good old HTML and CSS
  • imported an external library with npm an typings

Things we didn’t do:

  • write any backend code
  • define the “recipe” data type on the server or in a database

The code for this tutorial is on GitHub at albogdano/angular2-para. I’ve deployed the same code to GitHub pages as a live demo which is powered by our cloud-based Para service.

Have questions or suggestions? Chat with us on Gitter!