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:
1const appRoutes: Routes = [{ 2 path: '', 3 component: ContainerComponent, 4 children: [ 5 { path: '', component: HomeComponent }, 6 { path: 'article/:id', 7 component: ArticleContainerComponent, 8 children: [ 9 { path: '', redirectTo: 'details', pathMatch: 'full' }, 10 { path: 'details', component: ArticleDetailsComponent }, 11 { path: 'owner', component: ArticleOwnerComponent } 12 ] 13 }, 14 ] 15}];
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:
URL | Displayed components |
www.mysite.com/ | ContainerComponent->HomeComponent |
www.mysite.com/article/:id/details | ContainerComponent->ArticleContainerComponent->ArticleDetailsComponent |
www.mysite.com/article/:id/owner | ContainerComponent->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 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 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
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:
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.
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:
1@Component({
2 selector: 'app-base-data',
3 template: ''
4})
5export class BaseDataComponent {
6 @Input() data: Data[] = [];
7 @Output() select = new EventEmitter();
8
9 selectItem(data: Data) {
10 this.select.emit(data);
11 }
12}
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:
1styleUrls:['./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:
1export class DataTableComponent extends BaseDataComponent {
2 ngOnInit() {
3 super.ngOnInit();
4 }
5}
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.
1export class DataTableComponent extends BaseDataComponent {
2 selectItem(data: Data) {
3 super.selectItem(data);
4 console.log('Table item selected');
5 }
6}
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 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 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 @ContentChildre
n 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:
1:host ::ng-deep h1 {
2 color: red;
3}
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 and
. 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
.
More articles
fromEmir Ahmetovic
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Emir Ahmetovic
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.