Ionic 4 - search with service does not update page data after new data

Hi all,

I created a service for a search function (based on this Josh Morony’s tutorial )

All works fine, but need to implement a re-order of the list, this also works fine except the updating of the list to search in (page). Now the list only gets populated by the constructor, so it does not get updated (view) until you re-open the application. The update in the database (indexeddb) goes fine with the re-ordering. It is only the update of the page/view I am struggeling with.

How can I solve this ? So that after I re-ordered the list the view gets updated either directly or after re-visiting the page without closing the application.

Any hints ? Thanks.

Service code below :

import { Injectable } from '@angular/core';
import { openDB } from 'idb';

@Injectable({
  providedIn: 'root'
})

export class ActivitiesService {

  public activities: any = [];

  constructor() {
    this.fillDB();
  }

  async fillDB() {
    const db = await openDB(dbname, dbversion);
    this.activities = await db.getAll('activitycodes');
  }

  filterItems(searchTerm) {
    if (this.activities != null) {
           return this.activities.filter((activity) => {
        if (activity.description != null && activity.code != null) {
          return activity.description.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 || activity.code.indexOf(searchTerm) > -1;
        }
        return;
      });
    }
  }
}

You said you’re happy with the service, but not the view. So the code for the view would be helpful.

of course, here you go

import { Component, OnInit, ViewChild } from '@angular/core';
import { debounceTime } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
import { NavController, IonContent } from '@ionic/angular';
import { HttpService } from '../services/http.service';
import { ActivitiesService } from 'src/app/services/activities.service';
import { openDB } from 'idb';
import { IonReorderGroup } from '@ionic/angular';

@Component({
  selector: 'app-activities',
  templateUrl: './activities.page.html',
  styleUrls: ['./activities.page.scss'],
})
export class ActivitiesPage implements OnInit {

  @ViewChild(IonContent, { static: true }) content: IonContent;
  @ViewChild(IonReorderGroup, { static: true }) reorderGroup: IonReorderGroup;

  public activities: any;

  searchTerm: string = '';
  searchControl: FormControl;
  searching: any = false;
  noresults: any = false;
  constructor(
    private dataService: ActivitiesService,
    private httpService: HttpService,
    private navCtrl: NavController
  ) { this.searchControl = new FormControl(); }

  ngOnInit() {
  }

  ionViewDidEnter() {
    this.setFilteredItems();
    this.searchControl.valueChanges.pipe(debounceTime(500)).subscribe(search => {
      this.searchTerm = search;
      this.searching = false;
      this.setFilteredItems();
    });
  }

  onSearchInput() {
    this.searching = true;
  }

  setFilteredItems() {
    this.activities = this.dataService.filterItems(this.searchTerm);
    if (this.activities.length == 0 || this.activities.length == undefined) {
      this.noresults = true;
    } else {
      this.noresults = false;
    }
  }

  ScrollToTop() {
    this.content.scrollToTop(700);
  }

  ScrollToBottom() {
    this.content.scrollToBottom(1000);
  }

  async pushAct(code) {
    const db = await openDB(dbname, dbversion);
    await db.put('temp', { barcode2: code }, 2);
    this.navCtrl.navigateBack('/home');
  }

  async updatePresence() {
    this.httpService.updatePresence();
  }

  async onRenderItems(event: any) {
    console.log('Dragged from index', event.detail.from, 'to', event.detail.to);
    let draggedItem = this.activities.splice(event.detail.from, 1)[0];
    this.activities.splice(event.detail.to, 0, draggedItem)
    event.detail.complete();
    const db = await openDB(this.dbname, this.dbversion);
    await db.clear('presencecodes');
    {
      const tx = db.transaction('presencecodes', 'readwrite');
      let i = 0;
      for (i = 0; i < this.activities.length; i++) {
        tx.store.put(this.activities[i]);
      }
      await tx.done;
    }
    // console.table(this.activities)
  }

  toggleReorderGroup() {
    this.reorderGroup.disabled = !this.reorderGroup.disabled;
  }
}

and html

<ion-header>
  <ion-toolbar color="primary" mode="md">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/home" mode="md"></ion-back-button>
    </ion-buttons>
    <ion-title> Activity Codes </ion-title>
    <ion-buttons slot="primary">
      <ion-toggle color="light" (click)="toggleReorderGroup()"></ion-toggle>
    </ion-buttons>
  </ion-toolbar>
  <ion-toolbar color="primary">
    <ion-searchbar [formControl]="searchControl" (ionChange)="onSearchInput()" placeholder="Zoek hier ...">
    </ion-searchbar>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div *ngIf="searching" class="spinner-container" align="center">
    <ion-spinner name="dots" color="primary"></ion-spinner>
  </div>
  <div *ngIf="noresults" align="center">
    <br><br><br><br><br><br><br>
    <font size="5" color="#989aa2"><strong> no data </strong></font>
  </div>

  <ion-list>
    <ion-reorder-group (ionItemReorder)="onRenderItems($event)" disabled="true">
      <ion-item *ngFor="let activity of activities" (click)="pushAct(activity.code)">
        <ion-icon name="walk" slot="start"></ion-icon>
        <ion-label>
          <h2> {{activity.description}} </h2>
          <p style="font-size: 12px; color: rgb(175, 174, 174);"> {{activity.code}} </p>
        </ion-label>
        <ion-reorder slot="end"></ion-reorder>
      </ion-item>

    </ion-reorder-group>
  </ion-list>
  
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button>
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>

    <ion-fab-list side="start">
      <ion-fab-button color="secondary" (click)="updatePresence()">
        <ion-icon name="ios-refresh"></ion-icon>
      </ion-fab-button>
    </ion-fab-list>
    <ion-fab-list side="top">
      <ion-fab-button color="primary" (click)="ScrollToBottom()">
        <ion-icon name="ios-arrow-down"></ion-icon>
      </ion-fab-button>
      <ion-fab-button color="primary" (click)="ScrollToTop()">
        <ion-icon name="ios-arrow-up"></ion-icon>
      </ion-fab-button>
    </ion-fab-list>
  </ion-fab>
</ion-content>

I think this is an instance of Segal’s Law, in which somebody with two watches never knows what time it is. Having a single source of truth is something I always strive for, and one way you could go about dealing with this is outlined in this post: after reorder, poke the new array back to the service instead of just storing it locally. I would also do all the storage stuff in the service, instead of the page. If pages only concern themselves with presentation, delegating all data wrangling to services, maintenance becomes a lot easier because you can change data handling strategies by editing only one class.

Incidentally, there is a lot of any in here, that both makes the code hard to read (for an outsider, at least), and hides a lot of potential bugs. There’s also, I believe, a memory leak caused by failure to clean up the subscription made in ionViewDidEnter.

I changed the service to :

  public activities: Array<any> = [];

  constructor() {
  }

  async fillDB() {
    this.activities = [];
    const db = await openDB(dbname, dbversion);
    this.activities = await db.getAll('presencecodes');
  }

  async saveReorderedList(activities) {
    const db = await openDB(dbname, dbversion);
    await db.clear('presencecodes');
    {
      const tx = db.transaction('presencecodes', 'readwrite');
      let i = 0;
      for (i = 0; i < activities.length; i++) {
        tx.store.put(activities[i]);
      }
      await tx.done;
    }
  }

and to ts page to :

  public activities: Array<any> = [];

...

  searching: boolean = false;
  noresults: boolean = false;

..

  ionViewDidEnter() {
    this.dataService.fillDB().then(() => {
      this.setFilteredItems();
      this.searchControl.valueChanges.pipe(debounceTime(500)).subscribe(search => {
        this.searchTerm = search;
        this.searching = false;
        this.setFilteredItems();
      });
    });
  }

And now I get the page with the correct (and re-ordered) data. Thanks !

Only the memory leak you are talking about, there is seems to be no unsubscribe available on this:

this.searchControl.valueChanges.pipe(debounceTime(500)).subscribe(search => {

How do I do that ?

There is if you assign the result:

private vcsub?: Subscription;
ionViewDidEnter(): void {
  this.vcsub = this.searchControl.valueChanges.[...].subscribe(...);
}
ionViewWillLeave(): void {
  if (this.vcsub) {
    this.vcsub.unsubscribe();
    delete this.vcsub;
  }
}

Still learning here …

Thanks !!