Basic View-Model: Simple Component-Architecture Workflow with Angular2
Tags: angular, typescript, webdev, javascript
Published: 2022-04-29 23:19:43


<small>
Note: this is the documentation I wish I found when I first started learning Angular lolol.
</small>

---

### 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.
1. **General logic**: Get data from the data source to the template
2. **Conditional view logic**: Organize views that show how the data might be displayed in different ways (separate from data logic)
3. **User controls logic and effects**: Add view-model logic that interacts with data sources

---

## Skip ahead
* [General Logic: Data to Template List](#general-logic-data-to-template-list)
 * [Data Structure in Interface](#data-structure-in-interface)
 * [Getting Data from Service into Component File](#getting-data-from-service-into-component-file)
 * [Getting Data from the Component Template](#getting-data-from-the-component-template)
* [Conditional View Logic: Toggle User Views](#conditional-view-logic-toggle-user-views)
 * [Adding Conditional Variables to the Template](#adding-conditional-variables-to-the-template)
 * [Binding to the Component Itself](#binding-to-the-component-itself)
* [User Controls and Logic Effects](#user-controls-and-logic-effects)
 * [Template: User Control Binds the searchText Property](#template-user-control-binds-the-searchtext-property)
 * [Apply searchString Against Nested Array](#apply-searchstring-against-nested-array)

---

## 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]:

```typescript
// 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.
```typescript
// 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:
```typescript
<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.
```html
<!--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.
```typescript
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
```html
<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.
```typescript
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