Angular Components – An in-depth look

No Comments

The Angular framework has been around for a while and many common design patterns have been applied to it. This article will cover some of them and extend the view about Angular components, the core building blocks of Angular.

Routed/Parent-child Angular components

This is Angular’s default architecture design of organizing code into smaller reusable pieces. When the URL in the browser changes, the router searches for a corresponding route to determine the Angular components to display.

Here is an example of route definitions:

const appRoutes: Routes = [{
  path: '',
  component: ContainerComponent,
  children: [
    { path: '', component: HomeComponent },
    { path: 'article/:id',
      component: ArticleContainerComponent,
      children: [
        { path: '', redirectTo: 'details', pathMatch: 'full' },
        { path: 'details', component: ArticleDetailsComponent },
        { path: 'owner', component: ArticleOwnerComponent }
      ]
    },
  ]
}];

A global container is defined, ContainerComponent, which will be our parent Angular component for every page. It has two child routes (two pages): a home page and an article single page. Both pages are nested with the parent Angular component creating a hierarchical design. The article page defines one container component, ArticleContainerComponent, and two children: a details and an owner page.

The first child in the article page is defined as an empty route and will automatically navigate to the article details page. It will prevent the user from accidentally navigating to the ArticleContainerComponent, which has no data to present.

With this configuration the user will be able to access the following routes:

URLDisplayed components
www.mysite.com/ContainerComponent->HomeComponent
www.mysite.com/article/:id/detailsContainerComponent->ArticleContainerComponent->ArticleDetailsComponent
www.mysite.com/article/:id/ownerContainerComponent->ArticleContainerComponent->ArticleOwnerComponent

The router now knows how to nest our Angular components in regard to the entered URL. The second thing which needs to be defined is the HTML code so that Angular knows how to display the nested components. For this purpose the <router-outlet></router-outlet> directive is used. The router outlet acts as a placeholder that marks the spot in the template where the router should display the child components (official documentation). The ContainerComponent could be defined like this:

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

The <router-outlet></router-outlet> directive will render the home page component or the article container component in regards to the entered URL. The last thing is to place the <router-outlet></router-outlet> in the ArticleContainerComponent.

<div class="tab">
  <a href="/article/{{id}}/details">Details</a>
  <a href="/article/{{id}}/owner">Owner</a>
</div>
<router-outlet></router-outlet>

This example shows two tabs which allow to switch between the article details and the article owner component.

The components are now nested in a hierarchical style and they could be presented like a diagram:

Angular Components Routed Components

Sibling components cannot be displayed together in the view. The router displays components from the root to one leaf.

The data in all components should be fetched from services.

The final view of the example with this configuration is displayed in the following image:

Feature-presentation Angular components

A feature component is a top-level component which contains child presentation components. With this design, components are not nested hierarchically. Instead, the organisation is a flat styled component design with one feature container component and many smaller presentation components.

component design

Commonly, the feature component is a route component. It contains other components related to the same feature. The feature component is responsible for communicating with services and handling data (fetching, saving etc). Presentation components are supposed to keep low logic. They should have @Input properties to receive the data and @Output events, if necessary, to inform the feature component about user actions. They are similar to functions. With this organisation we make debugging easier because our main logic is in the top-level feature component.

An article browse page would be a perfect example of a feature-presentation design. The ArticleItemComponent will be the presentation component, and an ArticlesContainerComponent will be the feature component. The article item could be designed to receive the presentation data and to output an event when the item is selected.

<app-article-item 
   [item]="articleData"
   (select)="selectProduct($event)"></app-article-item>

The article feature component could be as simple as a for loop:

<div *ngFor="let article of articles>
  <app-article-item 
     [item]="article"
     (select)="selectProduct($event)"></app-article-item>
</div>

Presentation components do not have to be identical components. The feature component could hold a range of different presentation components related to the same feature.

This design enables us to reuse the feature component where the data needs to be displayed. The articles list feature could be used on the browse page, on the home page to display featured articles, on the user page to display favourites etc. Another trick is to use the feature component with differently designed presentation components, e.g. to highlight one article in the list.

Angular component inheritance

In OOP (Object-oriented programming), inheritance is used to take on properties from existing objects. This approach organizes around objects rather than actions, and data rather than logic.

In Angular it can be used to develop inherited components creating super and base components where the super component inherits all public methods and properties from the base component. The base component holds common reusable logic. It is known that services/providers are used to share data logic, but what if there is duplicate UI logic? UI logic is a perfect example of component inheritance. The base component can be used to hold reusable UI functionality and suppress code repeat in TypeScript.

One example of component inheritance would be a component which has two different presentation views. E.g. some data which should be presented in a table or a list depending on the screen size. A base class could be created to hold common UI functionality, like a  select method, to handle click events on the data items. The base component could be defined like this:

@Component({
 selector: 'app-base-data',
 template: ''
})
export class BaseDataComponent {
 @Input() data: Data[] = [];
 @Output() select = new EventEmitter();
 
 selectItem(data: Data) {
   this.select.emit(data);
 }
}

Notice that the template of the base component is empty. Template code is not inherited into the extended component. The extended component will implement its own template code. The two extended classes could be implemented like this:

@Component({
 selector: 'app-data-list',
 template: `
   <ul>
     <li *ngFor="let item of data">
       {{item.id}}
       <button (click)="selectItem(item)">Select</button>
     </li>
   </ul>
 `
})
export class DataListComponent extends BaseDataComponent {}
@Component({
 selector: 'app-data-table',
 template: `
   <table>
     <tr>
       <td>ID</td>
     </tr>
     <tr *ngFor="let item of data">
       <td>{{item.id}}</td>

       <td><button (click)="selectItem(item)">Select</button></td>
     </tr>
   </table>
 `
})
export class DataTableComponent extends BaseDataComponent { }

The extended components inherit only the public class logic. Private methods and properties are not inherited. All meta-data in the @Component decorator is not inherited, which makes perfect sense. The selector is a unique identifier and should not be shared with other components. The same is true for the template and styles, the new component is supposed to have its own UI, therefore we are inheriting it. A good trick to share common CSS is to create a CSS file in the base class and then include it in the styleUrls in the extended classes:

styleUrls:['./data-table.component.css', './data-base.component.css']

Properties with the @Input and @Output decorators are also inherited. In the example the base component has one @Input property to receive the data, and one @Output property to emit an event when an item has been clicked. Because these properties are inherited, the same rules apply for the inherited components. With this in mind, we can use our inherited components like this:

<app-data-list 
 [data]="articles"
 (select)="selectArticle($event)"></app-data-list>
 
<app-data-table
 [data]="articles"
 (select)="selectArticle($event)"></app-data-table>

Not so obvious is that lifecycle hooks are not inherited, like OnInit. Both the base and the inherited class should implement its own lifecycle methods. Because they are functions, they can be called at any time from the inherited class. In order to call them, we need to use the super keyword:

export class DataTableComponent extends BaseDataComponent {
 ngOnInit() {
   super.ngOnInit();
 }
}

It doesn’t have to be solely called inside of the ngOnInit, base lifecycle hooks can be called in any method.

One more handy tool is overriding methods. Inherited components can override base methods to extend functionality.

export class DataTableComponent extends BaseDataComponent {
 selectItem(data: Data) {
   super.selectItem(data);
   console.log('Table item selected');
 }
}

In the example above the table component has extended the selectItem method. It is using the super keyword to call the base class and keep all the base logic. This method does not have to be called. In the second line, the method starts to execute its own logic, e.g. to write some output to the console.

Component inheritance is a powerful way to solve the problem of duplicate UI code and keep clean code. It improves development speed and code maintenance. Inheritance could be used in combination with feature-presentation design, where extended components share common HTML code. This combination maximizes code reusability. Common functionality code is kept in the base component and common UI code is kept in presentation components.

Content projection

Is used to create configurable components, which means that users can insert custom content into the component. An example of content projection would be:

<my-component>
 <p>Custom content to insert</p>
</my-component>

In order to be able to accept custom content, a component must have the <ng-content> tag defined inside its template. For the example above the template could be defined as follows:

<div class="title">
 <h1>Welcome</h1>
</div>
<div class="body">
 <ng-content></ng-content>
</div>

Angular will render the custom content into the div with the body class. This allows the consumer to insert any content into the body. It is possible to have multiple <ng-content> tags to fully define which content should be placed where. The next example will improve the previous with a content projection in the title:

<div class="title">
 <ng-content select="h1"></ng-content>
</div>
<div class="body">
 <ng-content></ng-content>
</div>

The component will now search for a h1 tag and insert it into the title div. All other elements will be rendered in the body. An example how to use the component is shown below:

<my-component>
 <h1>Welcome</h1>
 <div>Content which will be rendered in the body</div>
</my-component>

If the custom content has no h1 element, it will render nothing in the title. When using the select directive, it is possible to look for an element with a given CSS class:

<ng-content select="input.test-class"></ng-content>

Projected content can be accessed within the content component with the @ContnetChild and @ContentChildren decorators. They are used the same way as @ViewChild and @ViewChildren, except that they can only access projected content.

<!-- Add a reference to the element, ‘titleElement’ -->
<h1 #title>Welcome</h1>

<!-- Use the reference to access the element -->
@ContnetChild(“title”) title: ElementRef;

<!-- Search for a specific component inside the projected content -->
@ContnetChild(MyCustomComponent) title: MyCustomComponent;

In the example above, the projected h1 element has received a template reference. In the content component this reference is used to access the projected child element. Also, we can access elements without a template reference, but with its class name.

Elements inside the projected content cannot be styled with regular CSS code in the template of the content component. Instead the :host ::ng-deep selector has to be used. The :host selector defines that the style will only be applied inside this component. The modifier ::ng-deep means that the style will also be applied to all descendant elements. This will allow to style elements which will be inserted into the component. In the above example to style the h1 element the CSS code could be defined as:

:host ::ng-deep h1 {
 color: red;
}

This will apply a red font color to all inserted h1 elements. Projected content can always be styled inside the consumer component to which they belong, with standard CSS code without any additional selectors or modifiers.

It is common that developers get confused between <ng-content> and <ng-container>. The first one is already explained, but what about the second one? As the name suggests, it is used as a container for HTML elements. It can be used if there is a span tag which is changing the CSS style and the behaviour is not wanted:

<p>This is <span *ngIf="veryNice">very</span> nice</p>
<p>This is <ng-container *ngIf="veryNice">very</ng-container> nice</p>

A select tag with for and if directives will break the HTML code:

<select [(ngModel)]="article">
 <span *ngFor="let a of articles">
   <span *ngIf="a.type !== 'PRIVATE'">
     <option [ngValue]="a">{{a.name}}</option>
   </span>
 </span>
</select>

<!-- <ng-container> to the rescue -->
<select [(ngModel)]="article">
 <ng-container *ngFor="let a of articles">
   <ng-container *ngIf="a.type !== 'PRIVATE'">
     <option [ngValue]="a">{{a.name}}</option>
   </ng-container>
 </spng-containeran>
</select>

Conclusion

The main focus of all approaches is to suppress duplicate code. All four ways are creating Angular components or pieces of code which are reusable. This will significantly reduce the amount of code and lead to applications which are easier to maintain. Fewer lines of code means also fewer bugs which can appear. This also enables faster development of new features. An application structured like this has a better organisation and overview.

This article did not mention Angular’s template directives to create reusable HTML code. These template directives are ngTemplate and ngTemplateOutlet.

Emir Ahmetovic

Emir is a software developer at codecentric Doboj since January 2019. He is working with Web Technologies and Resource Optimization Algorithms.

Comment

Your email address will not be published. Required fields are marked *