My first Angular application. Experience. Problems. Solutions.

Timothy
14 min readAug 12, 2018

--

Introduction

I’ve just recently finished my first Angular project. And I just want to share my experience with you guys. There were many problems I had to resolve. I hope when you read this article you will know what you might face when you write a significant web-application purely on Angular.

Project Overview

Its called SharingPlatter.com. Although from the initial view you probably won’t see how “deep the rabbit hole goes”. There is some brief information:

  • The frontend part (Angular) has more then 70k lines of code, and the back approx 30k lines (written on Express JS).
  • System utilises 3 main roles: Guests, Users, Restaurants.
  • Stripe Payments and Subscriptions hooked up
  • An entire backend control panel is written within application
  • Bootstrap 4 is used a primary front CSS framework, although PrimeNG components are used a lot as well.

Why Angular, not React or Vue?

Although, I didn’t even consider this moment when I began. I already knew that I want to build this application on Angular. I had an experience with VueJS before, and I was confident that this library is not enough for this project.

This is why I put Angular aside of others frameworks.

  • Angular is modular, its easily scalable,
  • Its astandalone framework which includes all required features (such as Routing, Interceptors, Lazy Loading, etc).
  • It can be used for a Hybrid Apps developments based on Ionic Framework.

Yet I don’t think it is wise to use Angular for a simple tasks, like a cart or product options selector. For this tasks React or Vue is better, easier and faster. But Angular overcomes others when it comes to complex web applications.

What database to use? MongoDB vs Firebase?

Of course, this question everyone asks. Should I use a Firebase? Its simple, its cloud, its Google :)

The main feature of my application is Geo Queering. The restaurants selects the region the map where he delivers to, hence when the customer enters the address which coordinates are within this region, the restaurants pops up in the search result. The regions are saves as GeometryCollection (polygons) in the db. Therefore the ability to process geolocations queries was the main reason for me to use MongoDB.

Yet initially aimed to use Firebase for this project. I spend weeks to find out an appropriate solution to structure the db so I can execute complex queries to it. But, unfortunately, there were no ways it can be accomplished. Even simple GeoQuery request with additional filters is not possible. Yet there are couple interesting solution, like Geofirex or Geofire to have a look at.

Application structure

Actually, I altered overall application structure more then 3 times, and I am very happy with the end result. As you remember the application consumes 3 user roles: guest, users, restaurants. Guests and Users share the same design, only functional difference, yet restaurants has their own control panel:

Luckily due to lazy-loading each portion can be loaded by demand. Therefore the end folder structure of this application looks as follows:

core
public
restaurant
user
shared

core contains core and singleton services for entire application, such as authentication, notification, titles, cart, etc.

public module has guests related components. In other words all public static pages, login and registration forms, etc.

restaurant module is a restaurant control panel (components, services)

user has registered users component and services (dashboard, orders, etc)

shared module is where the magic happens… This module has common components and services which shared between public and user modules.

Each of the following modules has its own routing sub-module.

Authentication and Roles based content

JWT is a primary authentication method for the application. HTTP Interceptor is used to handle authentication headers and error responses.

The important moment here is to know that interceptor will intercept all requests within the given scope of the application (each module can have its own interceptor, interceptors might inherit each other). In other words it will add a bearer token to every request within the application. I reckon you already see the critical security issue here… You need to apply auth token only to your API requests! Bear that in mind. It also handles responses from every third-parity requests. So if you will use a third party API’s the interceptor might misbehave while handling their error responses.

Structural Directives are lifesavers when you have to conditionally hide or show certain components. In my case, for example I had to hide Sign In, Sign Up buttons forms, Navigations items and other components. There are just a just a couple of directives which I used:

/**
* Structural directive hideForRoles
* Ex: <div *hideForRoles="['guest', 'user', 'restaurant']"><div>
* Hides elements for roles which are not listed in array
*/
@Directive({ selector: "[hideForRoles]" })
export class HideForRolesDirective implements OnInit, OnDestroy {
protected roles: string[] = [];
protected authStateSubscription: Subscription;
protected embeddedViewRef: EmbeddedViewRef<any>;
constructor(
private templateRef: TemplateRef<any>,
private authService: AuthService,
private viewContainer: ViewContainerRef
) {}
@Input()
set hideForRoles(roles: string[]) {
this.roles = roles;
}
ngOnInit() {
this.authStateSubscription = this.authService.authState.subscribe(
(authState: AuthState) => {
if (
authState.authenticated &&
authState.role &&
this.roles.includes(authState.role)
) {
this.viewContainer.clear();
} else if (!authState.authenticated && this.roles.includes("guest")) {
this.viewContainer.clear();
} else {
if (!this.embeddedViewRef)
this.embeddedViewRef = this.viewContainer.createEmbeddedView(
this.templateRef
);
}
}
);
}
ngOnDestroy() {
if (this.authStateSubscription) this.authStateSubscription.unsubscribe();
}
}
/**
* Structural directive guestOnly
* Ex: <div *guestOnly="true"><div>
* Hides elements for authenticated users
*/
@Directive({ selector: "[guestOnly]" })
export class GuestOnlyDirective implements OnInit {
protected state: boolean;
protected authStateSubscription: Subscription;
protected embeddedViewRef: EmbeddedViewRef<any>;
constructor(
private templateRef: TemplateRef<any>,
private authService: AuthService,
private viewContainer: ViewContainerRef
) {}
@Input()
set guestOnly(state: boolean) {
this.state = state;
}
ngOnInit() {
this.authStateSubscription = this.authService.authState.subscribe(
(authState: AuthState) => {
if (this.state && !authState.authenticated) {
if (!this.embeddedViewRef)
this.embeddedViewRef = this.viewContainer.createEmbeddedView(
this.templateRef
);
} else {
this.viewContainer.clear();
}
}
);
}
ngOnDestroy() {
if (this.authStateSubscription) this.authStateSubscription.unsubscribe();
}
}

This is to show the difference between restaurants front and back-end's and users navigation menus

LazyLoading

The production build for this application is around 8mb. Although, its not much, but there is no reason to load this chink to everyone. I mean authenticated users or restaurants utilise components which guests don’t. Thus their modules are lazyloaded by demand. As a result, if you are a guest user your application is only 2mb, as you sign up, an extra 2mb chunk is loaded or if you are a restaurant then 4mb chunk. This results in a significant loading speed and overall bandwidth usage.

Routing and Auth Guards

App component has nothing but router outlet only

<router-outlet></router-outlet>

This is because header and footer might be different in certain pages, for example restaurant signup page radically differs from other public pages.

Furthermore, because restaurant has its own control panel (with unique design) which not inherited, its the best approach. The restaurant can switch between customer front-end and its own back-end, because of that, to navigate through control panel additional router outlet is used inside restaurant component

<app-restaurant-header></app-restaurant-header>
<app-restaurant-sidebar></app-restaurant-sidebar>
<div class="content-wrapper">
<router-outlet name="page"></router-outlet>
</div>

Below is an the part of public Routes, it used data property to setup static titles (dynamic are set on component init function), roles property is used by auth structural directives.

const publicRoutes: Routes = [
{
path: "signup",
component: UserSignupComponent,
canActivate: [AuthService],
data: {
title: "Signup",
roles: ["guest"]
}
},
{
path: "login",
component: UserLoginComponent,
canActivate: [AuthService],
data: {
title: "Login",
roles: ["guest"]
}
},
{
path: environment.settings.user.backendPath,
loadChildren: "app/user/user.module#UserModule",
canLoad: [AuthService],
data: {
roles: ["user"]
}
}
];

Note, please read well knows discussion regarding lazy-loaded auxiliary routes to avoid potential issues.

Multiple HTTP Interceptors

I had a case when I have to use multiple interceptors. Auth Interceptor is used to apply auth headers to requests. Yet in restaurant module I used another interceptor which is used to handle subscriptions limitations (for example if current restaurant subscription exceeded its subscription limitations).I just imported core interceptor to restaurant module and added a custom interceptor to it. This is the beauty of good modular structure, its prevents code duplication.

import { CoreInterceptor } from "@core/core.interceptor";@NgModule({
...
providers: [
...
{ provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: RestaurantInterceptor, multi: true }
]
})

TypeScript — I love you

There is nothing like writing a good code. So imagine the following scenario — you are getting API requests in certain format:

export interface responseFormat {
data?: any;
message?: string;
}
/**
* Get Restaurant Info
*
* @return Promise<Restaurant>
*/
getRestaurant(): Promise<Restaurant> {
return this.http
.get<responseFormat>(environment.api + "/restaurant")
.map((res: responseFormat) => {
return res.data as Restaurant;
})
.toPromise();
}

Later on you have to change some information to the responded object properties, or delete a property, or perhaps, to change its type. TypeScript compiler immediately tells you all occurrences of altered objects properties and tell you mismatched ones. Saves you hours of debugging and testing time.

You just need to remember to always assign a class or interface to an object. The code becomes more readable and maintainable, for example this is the part of my auth service, beautiful, isn’t it?

export interface AuthState {
authenticated: boolean;
id?: string;
role?: string;
}
/**
* Getting Current Auth State
*
* @return AuthState
*/
private getAuthState(): AuthState {
const token = this.getToken();
if (token || !tokenHelper.isTokenExpired(token)) {
const payload: JwtPayload = jwt(token);
return {
authenticated: true,
id: payload.uid,
role: payload.type
};
}
return {
authenticated: false
};
}

SASS, LESS or CSS?

I didn’t have to decide which css pre-processor to use, I used Bootstrap 4, which is written on SASS, furthermore I love it more than LESS, although there were time when I used LESS, until I tried sass.

If you build Bootstrap 4 responsive UI. probably you will end to extend it with your own custom classes. I committed the collection of custom boostrap 4 mixins / additional classes to my repository, feel free to use it for your project.

Extending modules with dynamic injectable components

There are quite a few well knows components which used injection, fr instance ng-bootstrap modals, or ngx-toastr. I used it for payment modules. So in the future without no hassles I can add a new a payment method. Payment component looks as follows:

// Inside checkout component
<payment-methods (onChange)="paymentMethodChanged($event)" (onComplete)="paymentMethodCompleted($event)"></payment-methods>
// Payment Component Template
<div *ngIf="!paymentMethods" class="spinner"></div>
<ng-container *ngIf="paymentMethods && paymentMethods.length > 0;">
<div *ngFor="let payment of paymentMethods">
<p-radioButton name="payment" [value]="payment.type" [label]="payment.name" [(ngModel)]="selectedPaymentMethod" (ngModelChange)="initPaymentProcessor()"></p-radioButton>
</div>
</ng-container>
<div class="alert alert-warning" *ngIf="paymentMethods && paymentMethods.length == 0;">
No payment methods available
</div>
<template #paymentProcessorComponent></template>// Payment Component, the most interesting part
initPaymentProcessor() {
this.paymentProcessorComponent.clear();
const paymentProcessorComponent = this.paymentMethodsService.getPaymentComponents(
this.selectedPaymentMethod
);
let activePaymentMethod = this.paymentMethods.find(
method => method.type === this.selectedPaymentMethod
);
this.onChange.emit(activePaymentMethod);
if (paymentProcessorComponent) {
const factory = this.resolver.resolveComponentFactory(
paymentProcessorComponent
);
this.paymentProcessorComponentRef = this.paymentProcessorComponent.createComponent(
factory
);
this.paymentProcessorComponentSubscription = this.paymentProcessorComponentRef.instance.onComplete.subscribe(
result => {
let paymentMethodResult = activePaymentMethod;
paymentMethodResult.processorResult = result;
this.onComplete.emit(paymentMethodResult);
}
);
} else {
this.onComplete.emit(activePaymentMethod);
}
}
destroyPaymentProcessor() {
if (this.paymentProcessorComponentSubscription)
this.paymentProcessorComponentSubscription.unsubscribe();
if (this.paymentProcessorComponentRef)
this.paymentProcessorComponentRef.destroy();
}

So, from now on, I can create as many payment methods as I want.

Debugging and Error Handling

ErrorHandler can automatically catch and process errors. Its great because users can submit issues to repository automatically. Super easy to implement and saves you a lot of debugging time. To the client we are showing the message, while sending data to repository

// Errors Handler
@Injectable
()
export class ErrorsHandler implements ErrorHandler {
constructor(private injector: Injector) {}
handleError(error: Error | HttpErrorResponse) {
if (environment.error.logger) {
const errorsService = this.injector.get(ErrorsService);
errorsService.processError(error);
} else {
throw error;
}
}
}
// Errors Service
@Injectable()
export class ErrorsService {
constructor(private injector: Injector) {}
processError(error: Error) {
const location = this.injector.get(LocationStrategy);
const modalService = this.injector.get(NgbModal);
const onlineState: boolean = navigator.onLine; ... const errorModal = modalService.open(ErrorModalComponent, {
centered: true
});
...if (environment.error.logger && onlineState) {
...
const postUrl =
environment.error.endpoint +
"/projects/" +
environment.error.projectId +
"/issues?private_token=" +
environment.error.privateToken;
http.post(postUrl, {
title: "Error ID: " + hash,
description: message + "\n\n" + url + "\n\n" + error.stack,
labels: "auto"
})
...
}
}

System Update and Maintenance Status

It might be hard for a single page application to update it. Because its already in browser memory. Yet its not, specially when you use WebSockets this task gets easier.

As soon as client loads an application it connects to the server and from now on you can do anything to it.

  • You can send Server to Client Notifications,
  • Remotely Sign him Out
  • or to change his application maintenance status. As soon as application receives broadcasted maintenance message or 503 server status response it opens maintenance page to the client and listed to server update with an interval. As soon as server returns 200 status— you can:

return a client to the page where he was

this.location.back();

or to reload his application

location.reload(true);

Angular’s Search Engine Optimisation aspects

Google surprisingly good renders Angular applications. As long as you properly set page titles. This application has been indexed only once, yet its search output is already looks promising as you see

Responsiveness

Bootstrap 3 was a great framework, yet Bootstrap 4 is much better. Although, initially, I concerned about using it, due to its flexboxed nature, and the lack of support of non-evergreen browsers, but after some consideration I decided to use it.

There is just how nice and responsive UI is

Caching

Certainly you can debate whether you need to use caching nowadays or to bombard server with requests. In my case I had no option, information is not changed constantly (server is limited). Hence by caching some requests you can radially reduce server load, and increase user fronted experience. I’ve modified Ionic Cache to match my requirements. The result is — the server requests reduced by 70% and loading time is almost instant

Database Structure and Integrity

There one essential matter which shall be considered when you developing an application with NoSQL db. Because NoSQL dbs has no foreign keys hence you need to answer yourself “How are you going to keep referential integrity?”. In other words hope for the best, but be ready for the worst.

Event Sourcing is a great way to atomically update db entities, which is a lifesaver approach. In case you don’t understand the problem itself, here there is an example. Its a simplified version, which aims to give a brief understanding of the problem itself

// Category Collection Schema
CategorySchema {
_id: string;
name: string;
product: string; // Products References
}
// Products Collection Schema
ProductSchema {
_id: string;
name: string;
category: string; // Category References
}
addProduct(name: string, categories: string[]) {
// Create new product document
createProduct(name, categories)
.then(product => {
return addProductToCategory(product_id, category_id)
})
.then(() => {
// show success message
})
.catch(err => {
// show error message
})
}

In the example above referential incompleteness occurs when something went wrong (power disruption, memory, network, or something else) just before addProductToCategory function execution, but after createProduct one. It means that you’ve already added a product document into product collection, but its reference hasn’t been created into the category collection. Event Sourcing allows you to perform single atomic operation, and to continue event stack execution from the moment when something went wrong.

Although, it might case, I wasn’t using event sourcing technique for API implementation. So I have this problem, and perhaps, I will have to rewrite the back-end in a future.

If you concerned, how I implemented business hours in DB. I just store working hours in the following format:

export interface hours {
start: number //float
end: number //float
};
export interface days {
mon: hours[]
tue: hours[]
wed: hours[]
thu: hours[]
fri: hours[]
sat: hours[]
sun: hours[]
};
// see example bellow:
{
mon: [{start: 8.00; end: 13.00}, {start: 14.00; end: 18.00}]
tue: [{start: 14.00; end: 18.00}]
}

And it restaurant control-panel has user-friendly reactive form to manage them

Reactive vs Template Driven Forms

Almost 100% of all the forms I used are reactive. Template Driven can be used only for some small one (for example, login or password recovery form). This is to show you the form which allows users to create dynamic product options object.

export interface ProductOptionGroup {
_id: string;
name: string;
description: string;
multiple: boolean;
options: ProductOption[];
}
export interface ProductOption {
_id: string;
name: string;
price: number;
}
ngOnInit() {
this.productForm = this.formBuilder.group({
...
options: new FormArray([])
});
... // Populating Product Options
if (this.product.options && this.product.options.length) {
this.product.options.forEach((optionGroup, groupIndex) => {
this.addOptionGroup(false, true);
optionGroup.options.forEach(option => {
this.addOption(groupIndex, true);
});
});
}
... this.productForm.patchValue(this.product);

}
initOptionGroup(addOptionRow: boolean, addId: boolean) {
let group = this.formBuilder.group({
name: new FormControl("", Validators.required),
multiple: new FormControl(false),
description: new FormControl(""),
options: new FormArray(addOptionRow ? [this.initOption(addId)] : [])
});
// Important to add _id control, otherwise options ids will be regenerated again and again, which consequently causes cart product options inconsistency
if (addId) group.addControl("_id", new FormControl(""));
return group;
}
addOptionGroup(addOptionRow: boolean, addId: boolean) {
const optionGroup = <FormArray>this.productForm.controls.options;
optionGroup.push(this.initOptionGroup(addOptionRow, addId));
}
removeOptionGroup(g: number) {
let optionGroup = <FormArray>this.productForm.controls.options;
optionGroup.removeAt(g);
}
initOption(addId: boolean) {
let option = this.formBuilder.group({
name: new FormControl("", Validators.required),
price: new FormControl("", Validators.required)
});
if (addId) option.addControl("_id", new FormControl(""));
return option;
}
addOption(g: number, addId: boolean) {
const optionGroup = <FormArray>this.productForm.controls.options;
const option = <FormArray>optionGroup.controls[g]["controls"].options;
option.push(this.initOption(addId));
}
removeOption(g: number, i: number) {
const optionGroup = <FormArray>this.productForm.controls.options;
const option = <FormArray>optionGroup.controls[g]["controls"].options;
option.removeAt(i);
}

The back-end form result looks as follows

and the client front-end is

GeometryCollection Processing

In this application everything is around the ability an delivery to particular region. To select delivery region Google Map Drawing is used

To convert polygons to GeometryCollection and other geo-related operations TurfJS library applied. This is how easy to check if given point is within polygons with this library.

this.restaurant.branches.forEach((branch: Branch) => {
geomEach(branch.deliveryRegion, (geom: Polygon) => {
let featureCollection = pointsWithinPolygon(point, geom);
if (
featureCollection.features &&
featureCollection.features.length > 0
)
result = true;
});
});

Checkout Process

Checkout process has been simplified as much possible.

Furthermore there is no sensitive data is transferred to the serve. This is actually the only part where code duplication exists… The cart data is stored in DB, cart reference id is stored in LocalStorage. Every-time users opens the application cart data is loaded. During checkout process product and total prices are calculated locally. During the ckeckout only following data transferred to the server ( more info about reqParamHandler is here).

reqParamHandler({
cart_id: {
type: "string",
required: true
},
user_name: {
type: "string",
required: true
},
user_phone: {
type: "string",
required: false
},
delivery_method: {
type: "string",
required: true
},
delivery_address: {
type: "object",
required: false
},
delivery_note: {
type: "string",
required: false
},
payment_method: {
type: "object",
required: true
},
comment: {
type: "string",
required: false
}
})

On the server side we recalculate totals again.

Conclusion

I really enjoyed this project. An immense experience has been gained, great technologies learned.

If you have any additional questions — don’t hesitate to ask them.

If you found a typo or something else — please let me know. Orthography is not what I am good at.

Currently, I am looking for a new project. I have immense codebase which can easily be modified to meet practically any e-commerce requirements (like https://verado.com). With Angular you can create incredible, fast and responsive, user and search friendly web and hybrid web application.

--

--

Timothy

Web Developer / Designer, Search Optimiser, Project Manager, Motorcycle Mechanic, Engineer. All me, all in one…