DEV Community

Ria Pacheco
Ria Pacheco

Posted on

Basic View-Model: Simple Component-Architecture Workflow with Angular2


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.

  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

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.
}
Enter fullscreen mode Exit fullscreen mode

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;    
    });
  }   
}
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
    }  
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);      
      }    
    });  
  }        
}

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)