Card image cap

Understanding Modules, Directives and Components in Angular

Angular  • Posted one month ago

Modularity and structured code are the basic best practices you need to apply when writing a project code. A better organized code based on a perspective helps anyone better understand what your code is doing and why it exists. In Angular, where there can be a ton of pages, widgets and services created for a variety of sequences and interactions needs to be placed in an organized manner so as to manage things in a better way. Like how in standard languages we have namespaces for organizing classes in a logical way, Angular contains a specific conceptual feature called "ngModule" which does a similar job.

What are Modules?

A Module is a built-in feature in Angular, which helps group a set of components, directives, services or any other user developed feature together based on some common perspective. It configures the injector and the compiler, and describes how these features grouped under it must be compiled and injector be created at runtime.

The NgModule decorator and AppModule:

An NgModule is a general TypeScript class, but is decorated with an @NgModule decorator on top it, which earmarks the class as a Module. An NgModule decorator generally takes in three things:

  • declarations - all the features which are created and are grouped under this module, say like Components, Directives, Pipes and so on
  • imports - any Modules which need to be used in any of the features declared under this Module need to be "imported" via this section. For example - RouterModule, FormsModule, HttpClientModule and any other custom Modules whose declarations need to be used inside this Module are defined here
  • providers - any services or which are to be "injected" by the Angular Dependency Injector are declared in this section.

For example, in our OpenSocialApp we have two perspectives - a user perspective where we have user related features and functionalities, and posts perspective where we have all the components, pages and services that work for posts related functionality. As an Angular application grows in size with more and more things added, it becomes quite a mess to understand which is created for what purpose. To avoid this, we declare all the features related to "users" into an AuthModule that encapsulates all the things created for user's functionality. All the things created for "posts" functionality goes under PostsModule. When the application bootstraps, the Angular runtime goes in search for a particular Posts feature and the PostsModule tells it where and how the feature needs to be loaded, reducing the stress on the MainModule.

An Angular application typically contains at least one NgModule - which would be the AppModule, the Module which contains the root declarations of the dependencies, providers and features. It is first picked up by the Angular during execution which has the AppComponent class declared under it.

@NgModule({
  declarations: [
    AppComponent,
    NavbarComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes),
    HttpClientModule
  ],
  providers: [{
    provide: HTTP_INTERCEPTORS,
    multi: true,
    useClass: ErrorInterceptor
  },
  {
    provide: HTTP_INTERCEPTORS,
    useClass: PostsInterceptor,
    multi: true
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }

Creating Modules - PostsModule and AuthModule for separating Posts and Profile:

Like mentioned before, to create a Module we just need a TypeScript class with an NgModule decorator added on top. For example, let's assume we want to create a Module for our Posts section so that all things related to Posts functionality are organized here. The PostsModule looks like below:

@NgModule({
    declarations: [
        PostItemComponent,
        PostListComponent,
        NewPostComponent,
        SinglePostComponent
    ],
    imports: [
        CommonModule,
        PostsRoutingModule,
        FormsModule
    ],
    providers: []
})
export class PostsModule { }

Observe that we also have something called PostsRoutingModule apart from the CommonModule and FormsModule which are imported for using various built-in features of Angular inside the elements grouped under this Module.

Generally, we can declare Modules which serve different kinds of segregations such as:

  • A Domain Module, which separates all elements of a particular feature - like the PostsModule mentioned above
  • A Routing Module, which separates all the routing configuration for a specific Domain - like all the routing configurations needed for routing to the elements under the PostsModule are placed under the PostsRoutingModule which looks like below:
const routes: Routes = [
    { path: "posts", component: PostListComponent, canActivate: [AuthGuard] },
    { path: "posts/:id", component: SinglePostComponent },
    { path: "", component: PostListComponent, canActivate: [AuthGuard] }
]

@NgModule({
    imports: [
        RouterModule.forChild(routes),
    ],
    exports: [
        RouterModule
    ]
})
export class PostsRoutingModule { }
  • A Routed Module is where you add the configurations for Lazy-Loading NgModules. Lazy-Loading is a concept where a particular NgModule is not fetched into the runtime until its is invoked by a route. In such cases, the NgModule are configured at the root NgModule as below:
const routes: Routes = [
  { path: "posts", loadChildren: () => import("./posts/posts.module").then(m => m.PostsModule) },
  { path: "auth", loadChildren: () => import("./auth/auth.module").then(m => m.AuthModule) },
  { path: "", loadChildren: () => import("./posts/posts.module").then(m => m.PostsModule) },
  { path: "**", component: NotfoundComponent }
];

Similarly, the AuthModule which groups all the elements that are created for the User functionality is done as below:

@NgModule({
    declarations: [
        AuthCallbackComponent,
        ProfileComponent
    ],
    imports: [
        AuthRoutingModule,
        ReactiveFormsModule,
        FormsModule,
        CommonModule
    ]
})
export class AuthModule { }

where the routes belonging under the User functionality are configured as below:

const routes: Routes = [
    { path: 'auth-callback', component: AuthCallbackComponent },
    { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] }
];

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

Directives and Components:

Directives are a thing native to the Angular since the good old AngularJS times. These are one of the most fundamental and powerful features of Angular, which are only being made more powerful as the framework matures.

What are Directives?

Directives are Angular features which can help us modify the structural or behavioural aspects of the HTML elements on which these Directives are applied. We can apply a Directive on a HTML element and from within the Directive we can access the DOM of the HTML element using which we can play around with the element, such as modifying the attributes of the element, adding behaviour or even controlling the element itself. Angular provides us with many built-in "directives" which we have been using subconsciously in all our development. Remember the ngIf we use to controll if a span needs to be shown or not?

<li *ngIf="isLoggedInUser" class="nav-item">
	<a class="nav-link" [routerLink]="['/auth/profile']">Me</a>
</li>
<li *ngIf="!isLoggedInUser" class="nav-item">
	<a href="javascript:void()" class="nav-link" (click)="login()" tabindex="-1">Login</a>
</li>

We're controlling whether the li element needs to be loaded into the document or not; based on the isLoggedInUser boolean flag condition placed into the *ngIf attribute. This is one of the many "directives" we've been using all the time.

The Types of Directives:

Based on how the Directives work, we can categorize Directives into three major categories:

  • Structural

    • These directives control the HTML document, by adding or removing elements from the DOM based on some condition. For example - in the above code snippet, the first li is added to the DOM when the isLoggedInUser flag is set to true. When the flag sets to false, the li is immediately removed from the DOM.
    • Some other examples of Structural Directives are: ngIf, ngFor, ngSwitch.
    • All the Structural directives are prefixed by an asteric "*" to symbolize that they're structural directives.
  • Attribute

    • These directives modify the way the HTML elements behave in the DOM, say like how they appear in the document, their event listeners and so on.
    • These don't modify the HTML document in any way, and hence they don't possess any "*" to them. Some examples are: ngClass, ngStyle, ngModel

An Attribute directive looks like below:

import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appPostText]'
})
export class PostTextDirective {
  // do something on the DOM element 
  // on which this directive is applied
  constructor(private ref: ElementRef) {
    this.ref.nativeElement.style.color = '#555555';
  }
}

And is applied like this:

<div class="card my-2 rounded-0">
    <div class="card-body">
        <div class="container-fluid">
            <h5 class="card-heading">@{{post?.postedBy}}</h5>
            <p appPostText class="card-text">{{post?.text}}</p>
        </div>
    </div>
</div>

Like the name, its added as an attribute to the DOM element and applies to only the behavioral aspects of the element.

A Structural directive on the other hand fiddles with the element existence on the DOM. It looks something below:

@Directive({
  selector: '[appShouldShowText]'
})
export class ShouldShowTextDirective {

  private hasView = false;

  // check if the input Post object passed
  // has a PostText of minimum 20 characters
  // and display only those posts who meet 
  // the minimum criteria
  @Input() set appShouldShowText(post: Post) {
    let condition = post.text && post.text.length > 20;
    if (condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (!condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
  
  constructor(private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef) {
  }

}

And is applied as below:

<div *appShouldShowText="post" class="card my-2 rounded-0">
    <div class="card-body">
        <div class="container-fluid">
            <h5 class="card-heading">@{{post?.postedBy}}</h5>
            <p appPostText class="card-text">{{post?.text}}</p>
        </div>
    </div>
</div>

The directive picks the post object passed to it while the list is being rendered and then checks if the post text is minimum 20 characters long or not. If its not long enough, the post is ignored from rendering on the view.

A Component is a Directive:

The third category of Directives are actually Components. Surprising as it might seem, but Components are a special category of Directives which help in controlling a portion of the View. Since Components are a building blocks of Angular application Views with their own set of usecases and specializations, Components are often treated as something different from the other Directives.

"A Component encapsulates a piece of HTML view and renders incoming Model object on the View."

a Component is decorated by the @component attribute and so a general perception is that its different from a directive. But actually, a Component is a sophisticated directive which renders a non-existing HTML DOM on to the View, while the directives control an existing DOM.

import { Component, Input } from '@angular/core';
import { Post } from '../../post';

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

  @Input("post") post: Post;

  constructor() { }
}

A component class looks like above. You have the "selector" which describes how this component is added to the HTML, the templateUrl contains the path to the HTML file where the HTML view which this component controls exists. Alternatively, we can add the HTML inline using another parameter "template" which accepts the HTML as a string. The Styles related to this component alone, are placed under the "styleUrls" which can accommodate multiple CSS files.

When this component is compiled and the angular app runs, all these Style classes are merged into a single CSS file which the app loads under styles.css

An Angular application comes with a minimum one Component grouped under one Module - the AppComponent and the AppModule. The AppComponent is the root of the angular application on which all the child components are loaded.

<app-navbar></app-navbar>
<div class="container-fluid">
    <router-outlet></router-outlet>
</div>

To pass data to Components, we use the Input() parameters which form the attributes for these components in the HTML. We can pass any type of data to an Input() attribute based on how it is declared. In the above example of PostItemComponent, we have an Input() attribute post which accepts a Post object. Inside the PostList View, we render this PostListComponent under a Loop and pass the post object to this component within the loop.

<div class="container-fluid">
    <div class="row">
        <div class="col-md-8 mx-auto">
            <div *ngIf="isLoggedIn">
                <app-new-post></app-new-post>
            </div>
            <!-- routerLink with array params is called link parameters array -->
            <a class="text-body" 
				style="text-decoration:none;" 
				*ngFor="let post of posts;" 
				[routerLink]="['/posts', post?.id]" [queryParams]="{timestamp:'abcdefg'}">
                <app-post-item [post]="post"></app-post-item>
            </a>
        </div>
    </div>
</div>

Similarly, we can add events to the Components and do something once these events are triggered. These are added as Output() parameters. An Output parameter lets a child component pass on an event to the parent component for some action. For example, we can add an Output parameter on the PostItemComponent to let its parent component know whenever a mouse hovers over the rendered PostItemComponent element. Then the parent element can take action on it.

<div (mouseenter)="onHover()" 
	*appShouldShowText="post" 
	class="card my-2 rounded-0">
    <div class="card-body">
        <div class="container-fluid">
            <h5 class="card-heading">@{{post?.postedBy}}</h5>
            <p appPostText class="card-text">{{post?.text}}</p>
        </div>
    </div>
</div>

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

  @Input("post") post: Post;
  @Output("hover") hover = new EventEmitter();

  constructor() { }

  // emit an Event via the "hover"
  // output property whenever a mouse hovers
  // over the HTML
  onHover() {
    this.hover.emit();
  }
}

The parent which subscribes to this (hover) output property of the PostItemComponent and then takes an action whenever the event is triggered:

<div class="container-fluid">
    <div class="row">
        <div class="col-md-8 mx-auto">
            <button class="btn btn-primary rounded-0" type="button" 
				(click)="back()">Back</button>
            <app-post-item (hover)="highlight()" [post]="post"></app-post-item>
        </div>
    </div>
</div>

Inside the Parent component:

highlight() {
	alert("Hovered");
}

Final Thoughts:

Finally we're at the end of one huge article, which covers multiple basic topics of Angular. While NgModules provide the necessary encapsulation for the underlying angular elements based on a variety of user constraints or discretion, they free up the AppModule of all the configurations and make application look cleaner and lighter. The Directives are a basic to the Angular without which I think we don't have any other way to render our Views (Components are also Directives, remember). And we've seen how to create Structural as well as Attribute directives and wrapped up with the Components which have a lot more to offer than what we actually use them for. How are you planning to use these features for? Do let me know!

How do you detect browser in JavaScript / jQuery?
How can you protect your JS files in angular?
How would you implement a form having two components where one component updates another?
How can two components communicate with each other?
We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept