2

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>

2 Answers 2

6

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)
      )
  ));
}
Sign up to request clarification or add additional context in comments.

3 Comments

Hi @Michael D, thank you for taking time to answer the questing. I'm only using null to "reset" the filters. After I implemented your answer, the filtering works as I'd expect. Thank you very much! Would you mind tell me how did you put syntax highlighting to your code here? :) It's much more readable.
@Balu: Yes when using the triple-backticks method to add code, include the language name (typescript here) adjacent to the top backticks. Hit edit in my answer to see what I mean.
Most helpful Michael, once again, thank you very much. One thing, I wanted to mention, that I ended up using BehaviorSubject -s as tihe ReplySubject the filtering was not working (in case anybody would face that issue).
1

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

1 Comment

Hi @Arunkumar Ramasamy, thanks for your answer, but actually what I'm looking for here is not to use that if block and somehow combine those observable and return only the filtered one. Not sure if that make sense for you or if it's achievable at all. Thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.