How to allow multiple item selection (checkboxes) when using server side filtering and pagination through RXJS Subject

Viewed 609

I need to request data from my REST api, using Angular 4.3 HttpClient. My component uses Subject class to provide server side filtering (as the user types the term) and pagination, but I also need to allow multiple selection of items using the checkboxes in the results data grid.

I have the following code to create a data grid:

<tr *ngFor="let vehicle of vehicles$ | async">
    <td><input type="checkbox" name="vehicle-selection" id="vehicle-{{vehicle.id}}" value="{{vehicle.id}}" [ngModel]="vehicle.checked" (ngModelChange)="setChecked(vehicle.id, $event)" /></td>
    <td>{{vehicle.chassis}}</td>
    <td>{{vehicle.lastUpdate || '' | amFromUtc | amDateFormat: 'DD/MM/YYYY'}}</td>
    <td>{{vehicle.description}}</td>
    <td>{{vehicle.location}}</td>
</tr>

vehicles$ is an observable<Vehicle[]> and the data is displayed correctly, however, when I change the checked state for some checkboxes, the vehicle.checked property in the data bound observable is not changed.

What am I missing?

Here's the full component source:

import { Component, OnInit, AfterViewInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Router } from '@angular/router';

import { NotificationsService } from 'angular2-notifications';
import { ConfirmationService } from '@jaspero/ng2-confirmations';

import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/pluck';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/switchMap';


import { ResolveEmit, ConfirmSettings } from '../../shared/interfaces/jaspero';
import { VehiclesService } from '../../core/services/vehicles.service';
import { Vehicle } from '../../shared/interfaces/vehicle';
import { PagedResults } from 'app/shared/interfaces/paged-results';
import { distinctUntilChanged } from 'rxjs/operator/distinctUntilChanged';

@Component({
  selector: 'app-vehicles-search',
  templateUrl: './vehicles-search.component.html',
  styleUrls: ['./vehicles-search.component.scss'],
  providers: [VehiclesService]
})
export class VehiclesSearchComponent implements OnInit {
  vehicles$: Observable<Vehicle[]>;
  totalRecords$: Observable<number>;
  page = 1;
  searchTerm = '';

  searchTermStream = new Subject<string>();
  pageStream = new Subject<number>();

  constructor(
    private vehiclesService: VehiclesService,
    private router: Router,
    private notification: NotificationsService,
    private confirmation: ConfirmationService
  ) { }

  search(term: string) {
    this.searchTermStream.next(term);
  }

  assignAnomaly() {
    this.vehicles$
      .map(m => {
        console.log(m); return m.filter(f => f.checked);
      })
      .subscribe(res => {
        if (res.length < 1) {
          this.notification.warn('Associar Anomalia', 'Você deve selecionar um veículo antes.');
        }
      });
  }

  setPage(page: number) {
    this.pageStream.next(page);
  }

  ngOnInit(): void {
    const searchSource = this.searchTermStream
      .debounceTime(300)
      .distinctUntilChanged()
      .map(term => {
        this.searchTerm = term;
        return { search: term, page: 1 };
      });

    const pageSource = this.pageStream
      .map(pageNumber => {
        this.page = pageNumber;
        return { search: this.searchTerm, page: pageNumber };
      });

    const source = pageSource
      .merge(searchSource)
      .startWith({ search: this.searchTerm, page: this.page })
      .switchMap((params: { search: string, page: number }) => {
        return this.vehiclesService.getVehicles(params.search, params.page);
      });

      this.totalRecords$ = source.pluck('totalRecords');
      this.vehicles$ = source.pluck('results');
  }

  setChecked(val, ev) {
    console.log(val);
    console.log(ev);
  }
}

Http service

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';

import { PagedResults } from '../../shared/interfaces/paged-results';
import { Vehicle } from '../../shared/interfaces/vehicle';

import 'rxjs/add/operator/toPromise';
import { forEach } from '@angular/router/src/utils/collection';

@Injectable()
export class VehiclesService {

  private baseUrl = `${environment.apiUrl}/api/vehicles`;  // URL to web api

  constructor(private http: HttpClient) { }

  getVehicles(filter: string, page: number): Observable<PagedResults<Vehicle[]>> {
    filter = filter ? `/${filter}` : '';
    return this.http
      .get<Vehicle[]>(`${this.baseUrl}/page/${page}${filter}`, {observe: 'response'})
      .map((res) => {
        const totalRecords = +res.headers.get('X-InlineCount');
        const results = res.body;

        for (let i = 0; i < results.length; i++) {
          results[i].checked = false;
        }

        return {
          results: res.body,
          totalRecords: totalRecords
        };
      })
      .catch(this.handleError);
  }

  getVehicleById(id: number): Observable<Vehicle> {
    return this.http.get(`${this.baseUrl}/${id}`)
      .map((res: Response) => res.json())
      .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error);
    return Promise.reject(error.message || error);
  }
}
2 Answers

You can try to use an Array of Vehicle instead of the Observable, you start it empty, then subscribe to getVehicles, and add them to the array or replace them.

I think you should use [(ngModel)]="vehicle.checked" in the check box binding.

[ngModel]="vehicle.checked" is just one-way binding, it will just change the ui when the vehicle's checked property changed.

if you want the checkbox affect the checked property, you should used two-way binding , which is [(ngModel)]="vehicle.checked" .

Related