Balu
Balu

Reputation: 526

Angular dynamically filter RxJS observable with multiple condition

I want to achieve dynamic client side filtering with data from Angular FireStore.

I've the following service, which is working more or less, but it's really verbose and I think it can be improved. My problem is with the filteredFiles$ part, I think the if logic I'm using can be omitted with proper usage of RxJs operators, but I can't figure out how.

Any help would be appreciated.

Thank you.

The service:

files$: Observable<FileModel[]>;
public filteredFiles$: Observable<FileModel[]>;

public sourceFilter$ = new BehaviorSubject<string | null>(null);
public extensionFilter$ = new BehaviorSubject<string[] | null>(null);
public channelIdFilter$ = new BehaviorSubject<string | null>(null);
public teamIdFilter$ = new BehaviorSubject<string | null>(null);
public idFilter$ = new BehaviorSubject<string[] | null>(null);
private filesCollectionRef: AngularFirestoreCollection<FileModel>;

constructor(
    private afs: AngularFirestore,
    private userService: UserService
  ) {

this.filesCollectionRef = this.afs.collection<FileModel>('files', ref =>
  ref.where('perm.readers', 'array-contains', this.userService.uid));

this.files$ = this.filesCollectionRef.valueChanges({idField: 'id'});

this.filteredFiles$ = combineLatest([
  this.files$,
  this.extensionFilter$,
  this.channelIdFilter$,
  this.teamIdFilter$,
  this.idFilter$
]).pipe(
  map(([files, extension, channel, teamId, id]) => {
      if (extension === null && channel === null && teamId === null && id === null) {
        return files;
      } else {

        if (channel !== null && extension !== null && id !== null) {          
          return files.filter(
            (file) =>
              file.channel === channel && extension.includes(file.extension) && id.includes(file.id)
          );
        }

        if (extension !== null && id !== null) {          
          return files.filter(
            (file) =>
              extension.includes(file.extension) && id.includes(file.id)
          );
        }

        if (channel !== null && extension !== null) {          
          return files.filter(
            (file) =>
              file.channel === channel && extension.includes(file.extension)
          );
        }

        if (id !== null) {
          return files.filter(
            (file: FileModel) =>
              id.includes(file.id)
          );
        }

        if (extension !== null) {          
          return files.filter(
            (file: FileModel) =>
              extension.includes(file.extension)
          );
        }

        if (channel !== null) {          
          return files.filter(
            (file: FileModel) =>
              file.channel === channel
          );
        }
      }
    }
  )
);

filterByExtension(extensions: string[]) {
    this.extensionFilter$.next(extensions);
}

filterByChannelId(channelId: string | null) {
  this.channelIdFilter$.next(channelId);
}

filterByTeamId(teamId: string | null) {
  this.teamIdFilter$.next(teamId);
}

filterById(id: string[] | null) {
  this.idFilter$.next(id);
}

And in the template:

<li *ngFor="let file of this.fileService.filteredFiles2$ | async">
   <app-display-file [file]="file"></app-display-file>       
</li>

Upvotes: 2

Views: 6090

Answers (2)

Barremian
Barremian

Reputation: 31105

Multiple things could be adjusted here.

  1. Assuming you'll never push value null to any observables, you could replace BehaviorSubject with ReplaySubject(1) (buffer 1). This way you don't have to provide an initial value, but still be able to buffer a value and emit it immediately upon subscription.

  2. However if you might push null, then you could use ternary operator in-place to check if the value is defined.

Try the following

files$: Observable<FileModel[]>;
public filteredFiles$: Observable<FileModel[]>;

public sourceFilter$ = new ReplaySubject<string>(1);
public extensionFilter$ = new ReplaySubject<string[]>(1);
public channelIdFilter$ = new ReplaySubject<string>(1);
public teamIdFilter$ = new ReplaySubject<string>(1);
public idFilter$ = new ReplaySubject<string[]>(1);

private filesCollectionRef: AngularFirestoreCollection<FileModel>;

constructor(private afs: AngularFirestore, private userService: UserService) {
  this.filesCollectionRef = this.afs.collection<FileModel>('files', ref =>
    ref.where('perm.readers', 'array-contains', this.userService.uid)
  );
  this.files$ = this.filesCollectionRef.valueChanges({idField: 'id'});

  this.filteredFiles$ = combineLatest(
    this.files$, 
    this.channel$, 
    this.teamId$, 
    this.extension$, 
    this.id$
  ).pipe(map(
    ([files, channel, teamId, extensions, ids]) =>
      files.filter(file => 
        (channel ? file.channel === channel : true) &&
        (teamId ? file.teamId === teamId : true) &&
        (extensions ? extensions.includes(file.extension) : true) &&
        (ids ? ids.includes(file.id) : true)
      )
  ));
}

Upvotes: 6

Arunkumar Ramasamy
Arunkumar Ramasamy

Reputation: 781

You can go with else if instead of multiple if.

pipe(
  map(([files, extension, channel, teamId, id]) => {
      if (extension === null && channel === null && teamId === null && id === null) {
        return files;
      } else {
        const isChannel = Boolean(channel !== null);
        const isExtension = Boolean(extension !== null);
        const isId = Boolean(id !== null);

        if (isChannel && isExtension && isId) {          
          return files.filter((file) =>
              file.channel === channel && extension.includes(file.extension) && id.includes(file.id));
        } else if (isExtension && isId) {          
          return files.filter(
            (file) => extension.includes(file.extension) && id.includes(file.id));
        } else if (isChannel && isExtension) {          
          return files.filter(
            (file) => file.channel === channel && extension.includes(file.extension));
        } else if (isId) {
          return files.filter((file: FileModel) => id.includes(file.id));
        } else if (isExtension) {          
          return files.filter((file: FileModel) => extension.includes(file.extension));
        } else if (isChannel) {          
          return files.filter((file: FileModel) => file.channel === channel);
        }
      }
    }
  )

Upvotes: 1

Related Questions