Note: this is the documentation I wish I found when I first started learning Angular lolol.
Objectives
- Show a full list of database objects in a template (e.g. posts for a blog)
- Filter through the list by completing a search on every object's nested array (e.g. filter by tag or category sub-list)
- Ensure that all business and data logic are completely separated
Execution Workflow
This is just the best way I've found to do this.
- General logic: Get data from the data source to the template
- Conditional view logic: Organize views that show how the data might be displayed in different ways (separate from data logic)
- User controls logic and effects: Add view-model logic that interacts with data sources
Skip ahead
- General Logic: Data to Template List
- Conditional View Logic: Toggle User Views
- User Controls and Logic Effects
General Logic: Data to Template List
Data Structure in Interface
The data array's objects are called posts. This is the interface that defines its types [using the "I" prefix convention to indicate an interface]:
// post.interface.ts
export interface IPost {
id: string | number;
title: string;
body?: string;
cover?: string;
tags?: string[]; // <-- this is what we need to filter later.
}
Getting Data from Service into Component File
To bind the data to the template, we populate a new empty array called posts (to the type of IPost[]
) with data subscribed from the postService's getPosts()
method.
// post-list.component.ts
import { Component, OnInit } from '@angular/core';
import { PostService } from '../../services/post-service';
@Component({
selector: 'app-post-list',
templateUrl: './post-list.component.html',
styleUrls: ['./post-list.component.scss']
})
export class PostListComponent implements OnInit {
posts: IPost[]; // Initialize an empty array to the type of IPost[]
constructor(private postService: PostService) {}
/* When this component initializes, we access the data from the service's get method
and assign the stream of data to the empty posts array */
ngOnInit(): void {
this.postService.getPosts().subscribe(res => {
this.posts = res;
});
}
}
Getting Data from the Component Template
With angular's text interpolation, we add the post data into repeatable elements via the *ngFor directive:
<div *ngFor="let post of posts">
<div class="single-post">
{{ post.cover }}
{{ post.title }}
{{ post.tags }}
</div>
</div>
Conditional View Logic: Toggle User Views
Adding Conditional Variables to the Template
To show the user the full list of posts (without having to add too much to the component's code), we can use a conditional directive that toggles to a completely different list. This is done with the *ngIf/else directive, where we can tell the app to show an element if a certain condition is met [in this case, if the searchText property is empty], otherwise to show a completely separate one.
<!--Show this div if the searchText string is empty-->
<ng-container
*ngIf="searchText == '';else filteredListTemplate">
<div *ngFor="let post of posts">
<div class="single-post">
{{ post.cover }}
{{ post.title }}
{{ post.tags }}
</div>
</div>
</ng-container>
<!--Show this div as an alternative -->
<ng-template
#filteredListTemplate>
<!--Notice that 'let post of posts' is now 'let post of filteredPosts'-->
<div
*ngFor="let post of filteredPosts">
<div class="single-post">
{{ post.cover }}
{{ post.title }}
{{ post.tags }}
</div>
</div>
</ng-template>
Binding to the Component Itself
- Now we need to have the filteredPosts property, that we referenced in the template, exist on the component itself and specify it to the type of IPost[]. This way, we can run a function to return only the filtered posts to the filteredPosts array.
- We then add a searchText property to the type of string so that we have something to bind to, while simultaneously filtering the data.
export class PostListComponent implements OnInit {
posts: IPost[];
filteredPosts: IPost[]; // <-- new array to gather filtered posts
searchText = ''; // <-- needs to be empty to show the 'all' list of posts
constructor(private postService: PostService) {}
ngOnInit(): void {
this.postService.getPosts().subscribe(res => {
this.posts = res;
}
}
}
User Controls and Logic Effects
Template: User Control Binds the searchText Property
Now we want to populate the searchText string with our search keyword(s), so one way to do this is by adding click events with functions that pass through specific strings.
The filterPosts() method (that you'll see passing through the strings below) will be included in the following component section
<button (click)="filterPosts('science')">
Science
</button>
<button (click)="filterPosts('cooking')">
Cooking
</button>
<ng-container
*ngIf="searchText == '';else filteredListTemplate">
<div *ngFor="let post of posts">
<div class="single-post">
{{ post.cover }}
{{ post.title }}
{{ post.tags }}
</div>
</div>
</ng-container>
<ng-template
#filteredListTemplate>
<div *ngFor="let post of filteredPosts">
<div class="single-post">
{{ post.cover }}
{{ post.title }}
{{ post.tags }}
</div>
</div>
</ng-template>
Apply searchString Against Nested Array
The filter() method will return the compete array, so we apply the includes() method to the returned data.
export class PostListComponent implements OnInit {
posts: IPost[];
filteredPosts: IPost[];
searchText = '';
constructor(private postService: PostService) {}
ngOnInit(): void {
this.postService.getPosts().subscribe(res => {
this.posts = res;
}
}
filterPosts(tagStr: string): any {
this.searchText = tagStr; // bind searchText property to passed string
this.searchText = this.searchText.toLowerCase(); // make lowercase to avoid errors
this.filteredPosts = this.posts.filter(eachPost => {
const tagsArr = eachPost['tags'] // We access the nested array
const arrString = String(tagsArr).toLowerCase(); // convert object to string
if (eachPost && tagsArr) {
return arrString.includes(this.searchText);
}
});
}
}
That's it. That's all.
We understood how to think through the separation of logic by understanding:
- Specific data methods stay in their services/providers and we call them when the view-model needs it
- Taking advantage of Angular's template-driven features (directives, etc) instead of overloading the component with programmatic logic makes it a clean break if we need to refactor. After all, ripping out an html template is a lot less risky than refactoring a component's class (which might be interwoven in other component logic -- and in unpredictable ways)
Ria