rafi007akhtar / angular-course-projects

Contains lesson-wise code of many of the lessons in the online Angular course by Maximilian on Udemy (linked below). Also, contains notes of some of the topics I thought were important.

Home Page:https://www.udemy.com/course/the-complete-guide-to-angular-2/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Course Notes

Skip to:

Routing (Basics)

πŸ”

First, we need a routes array. As a best practice, this is often created inside its own module file, so create that.

ng generate module app-routing

Now inside this file (app-routing.module.ts), create a routes array.

  • On the most basic level, this array will be an array of objects each with two properties.
  • The first will be the path to the route
  • The next will be the component that needs to be shown at that route.

So it will be something like:

const routes: Array<Route> = [
  { path: "", component: HomeComponent },
  { path: "home", component: HomeComponent },
  {
    path: "users",
    component: UsersComponent,
    children: [{ path: ":id/:name", component: UserComponent }],
  },
  {
    path: "not-found",
    component: NotFoundComponent,
  },
  { path: "**", redirectTo: "/not-found" },
];

Other route propterties include:

  • children for nested routes
  • redirectTo for redirecting a path to another path.

Note. The last route in the above list (**) is called a wildcard.

Note. The first route can be rewritten as:

{ path: '', redirectTo: '/home', pathMatch: 'full' }

The pathMatch is needed so that it does not match every path containing '' (which is, basically, every path). By default, partial match is performed

When the routes array is ready, pass it as an import inside NgModule using RouterModule.forRoot method, and export the RouterModule.

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

Next, move over to the AppModule file and add this AppRoutingModule insdie the imports array.

Finally, go to the HTML file where the routing is needed (mostly app.component.html), and put the router-outlet directive where the routing needs to happen.

Passing static data

This is done using the data property of the route object. It holds an object which will contian the data. For example:

{
    path: 'not-found',
    component: NotFoundComponent,
    data: { message: 'Page not found.' },  // this line
},

This can then be retrieved in the component TS file through the activated route, either via snapshot or observable (below).

this.activatedRoute.data.subscribe((data: Data) => {
  if (data?.message) {
    this.errorMessage = data.message;
  }
});

Styling the active link

  • Attribute is routerLinkActive and value is the CSS class name for the active route.
<li role="presentation" routerLinkActive="active">
  <a routerLink="/servers">Servers</a>
</li>
  • If we want complete matching of the route (necessary for index page with "/" as the route), we pass the routerLinkActiveOptions and bind it to an object with exact specified as true.
<li
  role="presentation"
  routerLinkActive="active"
  [routerLinkActiveOptions]="{ exact: true }"
>
  <a routerLink="/">Home</a>
</li>

Routing Services

πŸ”

There are 3 kinds of routing services that I know of.

Auth Guards: Activated Guards

πŸ”

The first is the one that you activate. Its purpose is to block access to routes that are not activated. This can be useful when trying to block access to unauthenticated users.

To set this up, create a new service for this authguard.

ng g s auth-guard

This will create a service with AuthGuardService as the exportable class. Make it implement the CanActivate interface which needs to be imported from @angular/router.

export class AuthGuardService implements CanActivate {
  /* ... */
}

Now, this class needs to implement the CanActivate method. This method will contain the logic for the auth guard. If the method returns true, navigation to the guarded route will be allowed. Else, we are redirected to the home page.

canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    return this.authService.isAuthenticated().then((authState: boolean) => {
      if (authState) {
        return true;
      } else {
        this.router.navigate(['/']);
      }
    });
  }

The route and state types are imported from @angular/router.

This can also be extended to child routes by implementing the CanActivateChild interface. Simply call the above canActivate method in the canActivateChild method. Refer to the file for the full code.

To use this, just add this guard as a value to the canActivate or canActivateChild property in the route object to the routes that need guarding in the routing module file, like:

{
    path: 'servers',
    component: ServersComponent,
    canActivate: [AuthGuardService],
    canActivateChild: [AuthGuardService],
    children: [
      {
        path: ':id',
        component: ServerComponent,
      },
      {
        path: ':id/edit',
        component: EditServerComponent,
      },
    ],
},

Auth Guards: Deactivated Guards

πŸ”

The second auth guards are the ones for deactivation. Their purpose is to stop the user from navigating away from a route unless confirmation is received. For this, first create a service to house the interface for deactivation.

ng g s can-deactivate

Next, create an interface in this file inside this service that contains the method needed for deactivation, and export it.

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

Implement this interface inside the service class and call the component.canDeactivate method in it. (Its logic will be written in another file.)

canDeactivate(
    component: CanComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot
  ): boolean | Observable<boolean> | Promise<boolean> {
    return component.canDeactivate();
}

Next, add this deactivate guard in the route that needs it in the routing module file.

{
    path: ':id/edit',
    component: EditServerComponent,
    canDeactivate: [CanDeactivateService],
},

Finally, inside the TS file of the component that needs this guard, implement the above exported impterface. And write the logic for deactivation inside the canDeactivate function (or whichever function is present in the interface).

export class EditServerComponent implements OnInit, CanComponentDeactivate {
  /* ... */

  canDeactivate() {
    if (this.disableEdit || this.changesSaved) {
      return true;
    }

    if (
      this.serverName !== this.server.name ||
      this.serverStatus !== this.serverStatus
    ) {
      return confirm(
        "Looks like your changes are not saved. Are you sure you want to leave?"
      );
    }
  }
}

Essentially, the user will be allowed to leave the route when the canDeactivate method returns true.

Resolvers

πŸ”

These are used to get dynamic data in the in a route object (as opposed to data, which was for static data).

To use this in your application, first create the resolver service.

ng g s server-resolver

Next, implement the Resolve interface imported from @angular/router, and wrap it around the type of data you want to return (either synchronously or asynchronously).Inside this class, write the logic to return the dynamic data in the resolve method taken from the interface.

export class ServerResolverService implements Resolve<Server> {
  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Server | Observable<Server> | Promise<Server> {
    return this.serversService.getServer(+route.params.id);
  }
}

Next, put this resolver service inside an object to resolve property of a path object. In the inner object, this service should be the value of a key which will then be used to get the data in the component (here, that's server).

{
    path: ':id',
    component: ServerComponent,
    resolve: { server: ServerResolverService },
}

Lastly, use this data in the component TS file that needs it from the data property of the activated route object.

this.activatedRoute.data.subscribe((data) => {
  this.server = data.server;
});

Routing (Interceptors)

πŸ”

Interceptors are services that are used to:

  • intercept a request midway, modify it, then send the modified request,
  • tap on the response of the request received and perform common operations based on the response.

For this, first create a service file (manually, not using CLI) for the interceptor. For example, the file auth-interceptor.service.ts. In the class of this service, implement the HttpInterceptor interface imported from @angular/common/http.

Inside the class, write the intercepting logic in the intercept method from the interface. Here, the request is getting cloned, a new header is added to the cloned request, and the cloned request is going as the request using next.handle.

export class AuthIntercepterService implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const modifiedReq = req.clone({
      headers: req.headers.append("test", "123"),
    });
    return next.handle(modifiedReq);
  }
}

Next, add this service in the providers array of app module file. Keep in mind to:

  • add the class as a value of the useClass property
  • provide the HTTP_INTERCEPTOR token which is needed for this service.
{
    provide: HTTP_INTERCEPTORS,
    useClass: AuthIntercepterService,
    multi: true,
},

For tapping into the response and performing common operations, refer to this file.

Routing (Lazy Loading)

πŸ”

Lazy loading means when we navigate to a route, we load only the module associated with that route. Only when we visit another route do we load the module associated to the other route.

Thus, splitting the code into multiple modules is a prerequisite for lazy loading, at least in a module-based project (non-standalone).

The code for lazy loading is written in your routes array. The module that you want to be loaded lazily should be passed as an import function argument to the loadChildren attribute of a route object, and resolved as a promise, like this:

const routes: Routes = [
  {
    path: "route-address",
    loadChildren: () =>
      import("path-to-your-module").then((m) => m.NameOfModule),
  },
];

For example, in the routing module file of recipes, this is how the lazy-loading is written.

const routes: Routes = [
  { path: "", redirectTo: "/recipes", pathMatch: "full" },
  {
    path: "recipes",
    loadChildren: () =>
      import("./recipes/recipes.module").then((m) => m.RecipesModule),
  },
  {
    path: "shopping-list",
    loadChildren: () =>
      import("./shopping-list/shopping-list.module").then(
        (m) => m.ShoppingListModule
      ),
  },
  {
    path: "auth",
    loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule),
  },
];

Note. If you are lazily-loading a module, you should not load it eagerly as well. This means all the modules mentioned in the inside loadChildren functions of the routes array should not be added in the imports array of the app-module.ts file.

Pre-loading modules

πŸ”

This can be used to make sure after the initial small bundle is loaded, the app continues to load other lazily-loaded routes in the mean time, so that there is less delay when the user would visit those routes.

To implement this, import PreloadAllModules from @angular/router, and pass it as a value to preloadingStrategy in the forRoot method of the RouterModule, as shown below, and done in the same file.

@NgModule({
  imports: [
    RouterModule.forRoot(
        routes,
        { preloadingStrategy: PreloadAllModules }  // this line
    ),
  ],
  exports: [RouterModule],
})

NgRx (Modern Syntax)

πŸ”

Installations

First, install it with the following command.

ng add @ngrx/store

This will install the NgRx store package and modify the app.module.ts file like this:

imports: [
  // ...
  StoreModule.forRoot({}, {}),
  // ...
];

Later, side-effects will also be handled, with another package by NgRx. This can be installed like:

ng add @ngrx/effects

It will add the following change in the imports array of app.module.ts file:

imports: [
  // ...
  EffectsModule.forRoot([]),
  // ...
];

Store setup

Create a new folder for the store (not mandatory but considered a best practice), and inside of it create a reducer file (here, it's the counter.reducer.ts file.). Inside of this file, set up an initial state, and use it to create a reducer.

import { createReducer } from "@ngrx/store";
const initialState = 0;
export const counterReducer = createReducer(initialState);

Next, add this reducer in the app.module.ts file with a reducer name (key) and this reducer object (value), like:

StoreModule.forRoot({
  counter: counterReducer,
});

Using selectors

To get the value of a reducer from a store, a selector is needed. This is done by injecting the Store service in the constructor of the component where this value is needed, and passing the reducer name in the select method of its object.

In the counter project file, it is done like this:

count$: Observable<number>;
constructor(store: Store<counterModel>) {
    this.count$ = store.select('counter');
}

The value of this observable can then be shown in the HTML template file using the async pipe.

<p class="counter">{{ count$ | async }}</p>

We can also pass a function as parameter to the select method, but that is done below.

Creating actions

Actions are dispatched whenever the state of a reducer needs changing. While they don't contain the state change logic, they are linked to the said logic inside the reducer.

To create an action, we need an action name (first parameter), and an object that contains the value needed for the state change (second parameter) passed inside the createAction function. Make sure the action name is unique.

Example:

import { createAction, props } from "@ngrx/store";

export const increment = createAction(
  "COUNTER:INCREMENT",
  props<{ value: number }>()
);

As a best practice, create all actions in their own action file. Also, action names should be unique throughout the store (and not just reducer), so make sure to put the reducer name (here "COUNTER") in the action name along with its purpose (here"INCREMENT").

Linking actions to state change logic

This is done in the reducer. Inside the reducer, we can add multiple actions with their logic for state change in the on function.

The on function takes the action object (imported from the action ts file) as its first argument, and an inline function where the state changes as the second argument.

It can be written like this:

import { createReducer, on } from "@ngrx/store";
import { increment } from "./counter.actions";

const initialState = 0;

export const counterReducer = createReducer(
  initialState,
  on(increment, (prevState, props) => prevState + props.value)
);

Here, for the counter, we are setting the inital state as 0. Then, when the increment option is dispatched, we are increasing the value of the state by props.value. The previous state is automatically provided by NgRx as the first parameter of this inline function.

Dispatching actions

This is pretty simple. You just have to import the action from the actions TS file, and pass it to the dispatch method of the store object.

Syntax:

import { actionObject } from "path-to-actions-file";
import { Store } from '@ngrx/store';

// ... then inside the class
    constructor(private store: Store<typeOfStore>) {}

    methodForDispatchingAction() {
        const actionProp = {propName: propVal}
        this.store.dispatch(actionObject(actionProp));
    }

Example, in the counter controls file:

import { Store } from '@ngrx/store';
import { increment } from '../store/counter.actions';
import { counterModel } from '../store/store.model';

// ... then inside the class
  constructor(private store: Store<counterModel>) {}

  increment() {
    this.store.dispatch(increment({ value: 1 }));
  }

This method can be bound to a button in the HTML template file, so that pressig on it changes the state.

<button (click)="increment()">Increment</button>

Selector functions and layering

As a best practice, all selectors should be defined in their own file.

A selector, like seen above, can take a string parameter to get the selector from the store, but it can also take a function parameter. Such a selector is defined, for example, like this:

export const countSelector = (state: counterModel) => state.counter;

The paramter this function receives is the overall state of the store, and it returns the reducer specified. This approach is preferred over the string-based approach in complex applications where the same reducer is being used in multiple places, having a central location, like a selector file, to access it is more streamline.

We can then use it in the component file like this:

import { countSelector } from "../store/counter.selectors";

// then inside the constructor
this.count$ = store.select(countSelector);
// instead of: this.count$ = store.select('counter');

We can also layer selectors using the createSelector function, which takes the inline function as an argument. In this inline function we put multiple functions, and the return value of one the current function is the input to the next. The value returned in the final function is the return value of the overall function.

Example:

import { createSelector } from "@ngrx/store";
import { counterModel } from "./store.model";

export const countSelector = (state: counterModel) => state.counter;
export const doubleCountSelector = createSelector(
  countSelector,
  (state) => state * 2
);

Here, it is simply returning the doubled value of the value returned by the counter reducer.

Handling side-effects

A side-effect is any code we wish to execute when an action gets dispatched that does not directly result in a state change. For example, with the value of the state upon dispatching an action, we wish to send an HTTP request. This should not be written inside the reducer function, as it will result in unperceivable instability in the application. Instead, this should be handled separately, and here, with the effects.

To start, as a best practice, create a file containing all the effects.

Now, if we want to store the new state value in locala storage and log the action, for example, we will first have to create a class in this effects file. In this class, we inject the store and the actions, so something like:

import { Actions } from "@ngrx/effects";
import { Store } from "@ngrx/store";
export class CounterEffefcts {
  constructor(private actions$: Actions, private store: Store<counterModel>);
}

Now inside this class we create a new action called saveCount using createEffect function imported from @ngrx/effects. The first parameter it takes is an inline function, where we pipe the actions injected. Inside this pipe we pass some arguments: the first is calling ofType. We want this effect too happen every time the increment or decrement actions are dispatched, so we pass those two arguments here. Next, we want the action and counter data after these actions are dispatched. For that, we use withLatestFrom function and pass the counter selector. Finally, we write the logging and storing logic in the tap function, as shown below:

import { Actions, createEffect, ofType } from "@ngrx/effects";
import { decrement, increment } from "./counter.actions";
import { tap, withLatestFrom } from "rxjs/operators";
import { Store } from "@ngrx/store";
import { counterModel } from "./store.model";
import { countSelector } from "./counter.selectors";

@Injectable()
export class CounterEffects {
  saveCount = createEffect(
    () =>
      this.actions$.pipe(
        ofType(increment, decrement),
        withLatestFrom(this.store.select(countSelector)),
        tap(([action, counter]) => {
          console.log({ action });
          localStorage.setItem("count", counter.toString());
        })
      ),
    { dispatch: false }
  );

  constructor(private actions$: Actions, private store: Store<counterModel>) {}
}

The second parameter passed is {disptach: false} so that a new action is not dispatched once the effect is done (default is true). Lastly, this class is be made Injectable.

Reactive Forms (external)

πŸ”

Refer to these notes (drawn from the official docs): https://github.com/rafi007akhtar/angular-forms/blob/master/learning-notes.md

Unit testing (external)

πŸ”

Refer to these notes (drawn from the official docs): https://github.com/rafi007akhtar/angular-tests

About

Contains lesson-wise code of many of the lessons in the online Angular course by Maximilian on Udemy (linked below). Also, contains notes of some of the topics I thought were important.

https://www.udemy.com/course/the-complete-guide-to-angular-2/


Languages

Language:TypeScript 66.4%Language:HTML 22.8%Language:JavaScript 8.4%Language:CSS 2.4%Language:SCSS 0.1%