0

First of all, I am not a Front-end developer. I'm more specialized in back-end development. But, I have to do an app with Angular (I chose Angular 8). In previous work, I used Angular. But it's my first time I have to create an Angular app from scratch.

I have to read, add, modify and delete objects (called subscriptions). All this is fine.

Except when I wish to filter with a form...

To be more in the code, my page HTML is constructed that way :

subscription.component.html

<div id="subscription-filter">
    <app-subscription-filter></app-subscription-filter>
</div>

<div id="subscription-view">
    <app-subscription-view></app-subscription-view>
</div>

Where I have problems are with app-subscription-filter and app-subscription-view

subscription-filter part :

subscription-filter.component.html

<div class="row col-12">
    <div class="row col-11">
        <label class="col-1" for="selectCRNA">{{filterLabel}} </label>
        <select class="col-11 form-control" [(ngModel)]="selectedCRNA">
            <option *ngFor="let crna of filterSelection">
               {{crna.name}}
            </option>
        </select>
    </div>
    <div class="col-1">
            <button type="submit" class="btn"><i class="fas fa-search fa-lg" (click)="filterOnCRNAOnly()"></i></button>
    </div>
</div>

...

subscription-filter.component.ts

import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs';

import { SubscriptionService } from '../../shared/service/subscription.service';


@Component({
  selector: 'app-subscription-filter',
  templateUrl: './subscription-filter.component.html',
  styleUrls: ['./subscription-filter.component.css']
})
export class SubscriptionFilterComponent implements OnInit {

    filterLabel: string;
    filterSelection: any[];
    selectedCRNA: string;
    selectedCRNALabel: string;

    addSubscriptionForm : FormGroup;

    @ViewChild('closebutton', {static: false}) closebutton;

    constructor (protected subscriptionService: SubscriptionService) {

    }

    ngOnInit() {
        this.filterLabel = "Filtrer sur le tableau de résultat :";
        this.filterSelection = [
            { name: "Tous CRNA", value: "All" },
            { name: "CRNA NORD", value: "CRNA NORD" },
            { name: "CRNA SUD", value: "CRNA SUD" },
            { name: "CRNA EST", value: "CRNA EST" },
            { name: "CRNA OUEST", value: "CRNA OUEST" },
            { name: "CRNA SUD-OUEST", value: "CRNA SUD-OUEST" }
        ];

    }

    /**
     * Method to filter on CRNA selected
     */
    filterOnCRNAOnly() {
        console.log(this.selectedCRNA);
        this.subscriptionService.onlyCRNAFilterForSubject(this.selectedCRNA);
        this.selectedCRNALabel = this.selectedCRNA;
    }
}

...

subscription-view part :

subscription-view.html

<table class="table table-responsive table-hover" *ngIf="!!subscriptions || isLoadingResults">
    <thead>
        <tr>
            <th *ngFor='let col of tableHeaders'>
                {{col.header}}
            </th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor='let sub of (subscriptions)'>
            <td scope='row'>{{sub.status}}</td>
            <td>{{sub.region}}</td>
            <td>{{sub.provider}}</td>
            <td>{{sub.host}}</td>
            <td>{{sub.consumer}}</td>
            <td>{{sub.alias}}</td>
            <td>{{sub.filters}}</td>
            <td>
                <i class="fas fa-play mx-1" data-toggle="tooltip" title="Start subscription" (click)="startSubscription(sub, sub.id)"></i>
                <i class="fas fa-times mx-1" data-toggle="tooltip" title="Stop subscription" (click)="stopSubscription(sub, sub.id)"></i>
                <i class="fas fa-trash mx-1" data-toggle="tooltip" title="Delete subscription" (click)="deleteSubscription(sub.id)"></i>
            </td>
            <td></td>
        </tr>
    </tbody>
</table>

subscription-component.ts

import { Component, OnInit } from '@angular/core';

import { SubscriptionModel } from '../../shared/model/subscription.model';

import { SubscriptionService } from '../../shared/service/subscription.service';

@Component({
  selector: 'app-subscription-view',
  templateUrl: './subscription-view.component.html',
  styleUrls: ['./subscription-view.component.less']
})
export class SubscriptionViewComponent implements OnInit {

    subscriptions: SubscriptionModel[] = [];
    tableHeaders: any[];

    isLoadingResults = true;

    copiedSubscription: SubscriptionModel;

    constructor(protected subscriptionService: SubscriptionService) { }

    ngOnInit() {
        this.tableHeaders = [
            {field: 'status', header: 'Status'},
            {field: 'region', header: 'Region'},
            {field: 'provider', header: 'Fournisseur'},
            {field: 'host', header: 'Bus'},
            {field: 'consumer', header: 'Consommateur'},

            {field: 'alias', header: 'Alias'},
            {field: 'filters', header: 'Abonnement'},
            {field: '', header: 'Actions'},
            {field: '', header: 'Selections'}
        ];
        this.copiedSubscription = new SubscriptionModel();
        this.loadAll();
    }

    /**
     * Method to load all subscriptions
     */
    loadAll() {
       this.subscriptionService.initializeSubscriptions().subscribe((res: any) => {
            this.subscriptions = res;
            this.isLoadingResults = false;
       })
    }

    /**
     * Method to start a subscription
     * @param sub 
     * @param id 
     */
    startSubscription(sub: SubscriptionModel, id: string) {
        if (sub.status !== "OK") {
           this.subscriptionService.changeSubscriptionStatus(id, "ON");
        }
    }

    /**
     * Method to stop a subscription
     * @param sub 
     * @param id 
     */
    stopSubscription(sub: SubscriptionModel, id: string) {
        if (sub.status === "OK") {
            this.subscriptionService.changeSubscriptionStatus(id, "Off");
        }
    }

    /**
     * Method to delete a subscription
     * @param id 
     */
    deleteSubscription(id: string) {
        this.subscriptionService.deleteSubscription(id);
    }

}

I don't have (for the moment) any server call. All my datas are mocked with a JSON file. And the data which must be displayed is test$;

subscription.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';

import { Subject, Observable, of } from 'rxjs';
import { tap, filter, map, catchError } from 'rxjs/operators';

import { History } from '../model/history.model';
import { SubscriptionModel } from '../model/subscription.model';
import { Hosting } from '../model/hosting.model';


@Injectable({
  providedIn: 'root'
})
export class SubscriptionService {

    mockHostingUrl: string = 'assets/jsontests/hostmockdata.json';
    mockSubscribeUrl: string = 'assets/jsontests/subscriptionsmockdata.json';

    private test$: Subject<SubscriptionModel[]> = new Subject<SubscriptionModel[]>();

    private subsTest: SubscriptionModel[] = [];

    copiedSub: SubscriptionModel;

    crnaSelected: string = "Tous CRNA";

    constructor(private http: HttpClient) { }

    private handleError<T>(operation = 'operation', result?: T) {
        return (error: any): Observable<T> => {

            // TODO: send the error to remote logging infrastructure
            console.error(error); // log to console instead

            // Let the app keep running by returning an empty result.
            return of(result as T);
        };
    }

    /**
     * Method to initialize subscriptions
     */
    initializeSubscriptions() : Observable<SubscriptionModel[]> {
        return this.http.get<SubscriptionModel[]>(this.mockSubscribeUrl).pipe(tap((subs => {
            this.subsTest = subs;
            this.test$.next(this.subsTest);
        })));
    }

    /**
     * Method for adding a new subscription
     * @param sub 
     */
    addSubscription(sub: SubscriptionModel) {
        this.subsTest.push(sub);
        this.test$.next(this.subsTest);
    }

    /**
     * Method for changing subscription's status
     * @param id 
     * @param changeStatus 
     */
    changeSubscriptionStatus(id: string, changeStatus: string) {
        this.subsTest.find(element => {
            if (element.id === id) {
                element.status = changeStatus;
            }
        });

        this.test$.next(this.subsTest);
    }


    /**
     * Method to delete a subscription
     * @param id 
     */
    deleteSubscription(id: string) {
        this.subsTest.splice(this.subsTest.findIndex(element => element.id === id), 1);
        this.test$.next(this.subsTest);
    }

    /**
     * Method where there is the problem. It must filter and sending 
     * @param crnaSelected 
     */
    onlyCRNAFilterForSubject(crnaSelected: string) {
        console.log("dans onlyCRNAFilter");
        this.crnaSelected = crnaSelected;
        if (crnaSelected !== "Tous CRNA") {
            /*
            var temp = this.subsTest.filter(element => {
                element.region.includes(crnaSelected)
            });
            */
            console.log(this.subsTest);
            var temp: SubscriptionModel[] = [];
            this.subsTest.filter(
                element => {
                    console.log("---");
                    console.log(element);
                    if (element.region.includes(crnaSelected)) {
                        temp.push(element);
                        console.log("dedans!");
                    }
                }
            );
            console.log("apres fonction");
            console.log(temp);
            this.test$.next(temp);
        } else {
            console.log(this.subsTest);
            this.test$.next(this.subsTest);
        }
    }

}

When I try to filter my table, I do have the right datas, but my HTML doesn't refresh with the correct data

Logger for debug

I must confess I don't know what to do anymore... So, a bit help will be grateful.

Thanks in advance.

(Sorry for my English, it's not my native language)

1 Answers1

0

update, this solution works only if the two components are not rendered in the same time

in subscription view component, you subscribe to the initialize method only in the service

so if some change has happen in onlyCRNAFilterForSubject method in service, you will not be aware of it

we can add a Boolean property to the service to indicate if we are in the filtering mode or not, and set this property in the subscription filter component while we call the service

so instead of subscribing to the initialize method all the time, we could subscribe to the test$ observable if we are in filtering mode

so in the service we need to define a new Boolean property like that

isFilteringMode: Boolean = false; // set it initailly to false, to get all the data once the component is loaded at the first time

and in the subscription filter component, we need to set this property to true once some CRNA is selected

filterOnCRNAOnly() {
    console.log(this.selectedCRNA);
    this.subscriptionService.isFilteringMode = true; // enable the filtering mode
    this.subscriptionService.onlyCRNAFilterForSubject(this.selectedCRNA);
    this.selectedCRNALabel = this.selectedCRNA;
}

in subscription view component, in initialization mode, we will subscribe to the service function that returns the whole array, and when some CRNA is selected (filtering mode), we can subscribe to the test$ observable,

so in the subscription view component, it will be something like that

ngOnInit() {
    this.tableHeaders = [
        { field: 'status', header: 'Status' },
        { field: 'region', header: 'Region' },
        { field: 'provider', header: 'Fournisseur' },
        { field: 'host', header: 'Bus' },
        { field: 'consumer', header: 'Consommateur' },

        { field: 'alias', header: 'Alias' },
        { field: 'filters', header: 'Abonnement' },
        { field: '', header: 'Actions' },
        { field: '', header: 'Selections' }
    ];
    this.copiedSubscription = new SubscriptionModel();
    this.loadAll();
}

/**
 * Method to load all subscriptions
 */
loadAll() {
    if (this.subscriptionService.isFilteringMode) {
        // we are in the filtering mode
        // here we will subscribe to test$ observable to get the filtered data
        this.subscriptionService.test$.subscribe((res: any) => {
            this.subscriptions = res;
            this.isLoadingResults = false;
        });

    } else {
        // we are not in the filtering mode
        // so get all the data
        this.subscriptionService.initializeSubscriptions().subscribe((res: any) => {
            this.subscriptions = res;
            this.isLoadingResults = false;
        })
    }
}

Update, here is a similar mini-project like your one

here we have a list component which lists some users, and a filter component which used to select some status to filter the candidates in the list component

we can use a BehaviorSubject to carry the selected status from the filter component to the list component

BehaviorSubject is very useful in such a case as it holds an initial value and we don't need to wait till subscribe to it to get the value

here you can find the difference Subject vs BehaviorSubject

here are the files

app.component.html

<div class="row container">
  <div class=" row col-md-12">
    <div id="subscription-filter">
      <app-candidates-filter></app-candidates-filter>
    </div>

  </div>

  <div class="row col-md-12" style="margin-top: 30px;">
    <div id="subscription-view">
      <app-candidates-list></app-candidates-list>
    </div>

  </div>
</div>

candidates-list.component.ts

import { Component, OnInit } from '@angular/core';

import { SharedService } from '../shared.service';
import { User } from '../user.model';

@Component({
  selector: 'app-candidates-list',
  templateUrl: './candidates-list.component.html',
  styleUrls: ['./candidates-list.component.css']
})
export class CandidatesListComponent implements OnInit {

  originalCandidatesList: User[];
  candidatesList: User[];

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
    console.log('in candidates list component');

    this.sharedService.selectedStatusObs.subscribe((selectedStatus: number) => { // subscribe to the selected status first
      console.log(selectedStatus);
      if (!selectedStatus) {
        // no status has been selected

        console.log('no status selected');

        this.sharedService.getAllCandidatesV2().subscribe((res: User[]) => { // get all the users from the service
          // console.log(res);
          // console.log(typeof(res));
          this.originalCandidatesList = res;
          this.candidatesList = res;
        });

      } else {
        // some status has been selected
        console.log('some status selected >>> ', this.sharedService.statuses);

        const selectedStatusObj = this.sharedService.statuses.find(item => item.code === +selectedStatus);

        console.log('selectedStatusObj >>> ', selectedStatusObj);

        // just getting the selected status candidates
        this.candidatesList = this.originalCandidatesList.filter(item => item.status === selectedStatusObj.name);
      }
    });

  }

}

candidates-list.component.html

<table class="table table-responsive table-hover">
  <thead>
    <tr>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Email Address</th>
      <th>Age</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor='let can of candidatesList'>
      <td>{{ can.firstName }}</td>
      <td>{{ can.lastName }}</td>
      <td>{{ can.emailAddress }}</td>
      <td>{{ can.age }} years</td>
      <td>{{ can.status }}</td>
    </tr>
  </tbody>
</table>

candidates-filter.component.ts

import { Component, OnInit } from '@angular/core';

import { SharedService } from '../shared.service';

@Component({
  selector: 'app-candidates-filter',
  templateUrl: './candidates-filter.component.html',
  styleUrls: ['./candidates-filter.component.css']
})
export class CandidatesFilterComponent implements OnInit {
  statuses = [];
  selectedStatus: number;

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
    console.log('in candidates filter component');
    this.statuses = this.sharedService.statuses;
  }

  filterCandidates() {
    console.log(this.selectedStatus);
    this.sharedService.selectedStatusObs.next(this.selectedStatus);
  };

  resetFilters() {
    // emil null to the selectedStatus Observable
    this.sharedService.selectedStatusObs.next(null);
  }

}

candidates-filter.component.html

<div class="row col-xs-12">
  <div class="row col-xs-8">
      <label class="col-xs-3">Select Status </label>
      <select class="col-xs-9 form-control" [(ngModel)]="selectedStatus">
          <option *ngFor="let status of statuses" [value]="status.code">
             {{ status.name }}
          </option>
      </select>
  </div>
  <div class="col-xs-4" style="margin-top: 25px;">
    <button type="submit" class="btn btn-primary" (click)="filterCandidates()">Search</button>
    <button type="submit" class="btn btn-default" (click)="resetFilters()">Reset</button>
  </div>
</div>

shared.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject, of } from 'rxjs';
import { User } from './user.model';

@Injectable({ providedIn: 'root' })

export class SharedService {

  selectedStatusObs = new BehaviorSubject<number>(null); // this is the observable to watch the selected status in the filter component

  candidates: User[] = [ // a list of users
    {
      firstName: 'Thierry',
      lastName: 'Henry',
      emailAddress: 'henry@test.com',
      age: 1,
      status: 'Active'
    },
    {
      firstName: 'Alexis',
      lastName: 'Sanchez',
      emailAddress: 'sanchez@test.com',
      age: 28,
      status: 'Idle'
    },
    {
      firstName: 'Denis',
      lastName: 'Bergkamp',
      emailAddress: 'Bbrgkamp@test.com',
      age: 29,
      status: 'Active'
    },
    {
      firstName: 'Jerman',
      lastName: 'Defoe',
      emailAddress: 'defoe@test.com',
      age: 22,
      status: 'Active'
    },
    {
      firstName: 'Kun',
      lastName: 'Aguero',
      emailAddress: 'aguero@test.com',
      age: 25,
      status: 'Offline'
    },
  ];

  statuses = [
    {
      code: 1,
      name: 'Active'
    },
    {
      code: 2,
      name: 'Offline'
    },
    {
      code: 3,
      name: 'Idle'
    }
  ]

  getAllCandidatesV2() {
    return of(this.candidates); // trying to mock the HTTP request via this 'of' observable
  }

}

user.model.ts

export class User {
  constructor(
    public firstName: String,
    public lastName: String,
    public emailAddress: String,
    public age: Number,
    public status: String
  ) { }
}
Mohammed Yousry
  • 2,134
  • 1
  • 5
  • 7
  • Hello Mohammed, Your suggestion was correct. Also, thank you for your explanations, it was very useful :) Just a thing, which I found quite disturbing (but maybe it's normal in Angular). I still have to subscribe to intialization in loadAll(). Else, I don't have the data at the start... Do you have an idea why ? loadAll() { this.subscriptionService.test$.subscribe(...); this.subscriptionService.getSubscriptions().subscribe(); } – Cantarelle Apr 08 '20 at 12:09
  • ah yes, you are right, as if this component is opened without any filtering, it should return all the data, we should subscribe to the test$ observable only if some filtering exists, I've just updated my answer, could you check it now? – Mohammed Yousry Apr 08 '20 at 12:42
  • I just tested and it doesn't work. I guess it's because I only call loadAll() when I'm in the page, at the beginning. However, when I change my filter, my component-view isn't aware that test$ changed. – Cantarelle Apr 08 '20 at 13:08
  • did you set this.subscriptionService.isFilteringMode = true; in the subscription filter component while you call this method filterOnCRNAOnly? – Mohammed Yousry Apr 08 '20 at 13:33
  • Yes I did. That's why I think it's because as loadAll() isn't call anymore after ngOnInit() that the view isn't updated. – Cantarelle Apr 08 '20 at 14:14
  • could you create a snapshot of your project on stack blitz ? https://stackblitz.com/ – Mohammed Yousry Apr 08 '20 at 14:16
  • hmm, I thought the subscription view component is loaded once you apply the filter that's why I made it depend on ngOnInit(), but it seems the component is loaded with the subscription filter component, that's why we cannot see the effect of changing test$, let me think about it again – Mohammed Yousry Apr 08 '20 at 14:31
  • Unfortunately, I can't put the code in stackblitz or anything like that because my client doesn't want to. It was hard to conviced him at first to put some code in stackoverflow. But, what we are doing now, it's just to do the code more beautiful ^^. – Cantarelle Apr 08 '20 at 14:46
  • @Cantarelle, I've updated my answer again with a mini-project that simulates your search filter, hope it will be clear for you, and solves your problem – Mohammed Yousry Apr 08 '20 at 23:47