collectionCount not displaying value in template / meteor-rxjs in a service

334 Views Asked by At

That's my first submission to SO, so, if anything is wrong or not in the right place, please feel free to tell me.

Now to my question:

I'm trying to implement a service within a simple to-do application based on the Angular2 meteor-base boilerplate.

Consider the following code, where I'm trying to do two things:

  • Display a bunch of to-do lists ( <- This works )
  • Display the number of lists present with .collectionCount() ( <- This doesn't work )

todolist.service.ts:

import { Injectable } from '@angular/core';
import { Subscription, Observable } from 'rxjs';
import { MeteorObservable, ObservableCursor } from 'meteor-rxjs';

import { Todolist } from '../../../../../both/models/todolist.model';
import { Todolists } from '../../../../../both/collections/todolists.collection';

@Injectable()
export class TodolistService {

  todolistSubscription: Subscription;
  todoLists$: Observable<Todolist[]>;
  numLists$: Observable<number>;

  constructor() {
    this.initListSubscription();
  }

  initListSubscription() {
    if (!this.todolistSubscription) {
      this.todolistSubscription =     MeteorObservable.subscribe("todolists").subscribe(() => {
        // Code to be executed when the subscription is ready goes here
        // This one works
        this.todoLists$ = Todolists.find({}).zone();
        this.todoLists$.subscribe((lists) => {
          console.log(lists);
        });
        // This one doesn't
        this.numLists$ = Todolists.find({}).collectionCount();
        this.numLists$.subscribe((numberOfLists) => {
          console.log(numberOfLists);
        })
      });
    }
  }

  getLists(selector?, options?) {
    // Just in case anyone is wondering what those params are for...
    // return Todolists.find(selector || {}, options || {});
    return this.todoLists$;
  }

  getListsCount() {
    return this.numLists$;
  }

  unsubscribeFromLists() {
    this.todolistSubscription.unsubscribe();
  }

}

This one, I import in my app.module.ts and add it to the providers-array.

Then, in my list.component.ts I use the service like so:

import { Component, OnInit } from '@angular/core';
import { TodolistService } from '../../shared/todolist.service'
// + all other relevant imports, e.g. Todolist (the model), Todolists (collection)

@Component({
  selector: 'list-component',
  template,
  styles: [style]
})

export class ListComponent implements OnInit{

  lists$: Observable<Todolist[]>;
  numLists$: Observable<number>;

  constructor(private _todolistService: TodolistService){}

  ngOnInit(){
    // Again, this one works...
    this._todolistService.getLists().subscribe((lists) => {
      console.log(lists);
    });

    // ...and this does not
    this._todolistService.getListsCount().subscribe((number) => {
      console.log(number);
    });

    // This I can also use in my template, see below...
    this.lists$ = this._todolistService.getLists();

    // This one I can't
    this.numLists$ = this._todolistService.getListsCount();
  }

}

todolist.component.html:

In my template I for example do the following:

<!-- This one works... -->
<div *ngFor="let list of lists$ | async">{{list._id}}</div>

<!-- This one doesn't... -->
<span class="badge">{{ numLists$ | async }}</span>

Things I tried:

  • adding the .zone()-operator to the method defined in my service, like
      getListsCount() {
        return this.numLists$.zone();
      }
  • Tried the same (that being the addition of the .zone()-operator) in the initListSubscription()-method of the service, where I do things when the subscription is ready
  • Tried the same in my component, when I call
    // with the addition of .zone()
    this.numLists$ = this._todolistService.getListsCount().zone();

=====

Adding .zone() was, from my point of view as someone who does this as a hobby, the obvious thing to do. Sadly, to no effect whatsoever. From what I understand, this attaches the asynchronus task that's happening to angulars zone and is basically the same as saying

constructor(private _zone: NgZone){}

ngOnInit(){
  this._zone.run(() => {
  //...do stuff here that's supposed to be executed in angulars zone
  })
}

for example.

Can someone help me out? I really tried to understand what's going on, but I can't wrap my head around, why I'm not able to get the actual number of lists out of that observable.

Another thing I'm wondering:

If I were to do all of this directly in my component and I wanted my list to update automatically if I added new to-dos, I'd do the following to make things reactive:

MeteorObservable.subscribe("todolists").subscribe(() => {
  // The additional part that's to be executed, each time my data has changed
  MeteorObservable.autorun().subscribe(() => {
    this.lists$ = Todolists.find({}).zone();
    // with/without .zone() has the same result - that being no result ...
    this.listCount$ = Todolists.find({}).collectionCount();
  });
});

Here, I also can't wrap my head around how to achieve reactivity from within my service. I tried this, and again for the to-do lists it's working, but for the .collectionCount() it's not.

I'd really appreciate if some could point my in the right direction here. Maybe I'm missing something, but I feel like this, in theory, should work, since I'm able to get the lists to display (and even update reactively, when I do things from within my component).

Thanks in advance!

UPDATE:

Thanks to @ghybs I finally managed to get a working solution. Below you find the final code.

todolist.service.ts:

import { Injectable } from '@angular/core';
import { Observable, Subscription, Subject } from 'rxjs';
import { MeteorObservable, ObservableCursor } from 'meteor-rxjs';

import { Todolist, Task } from '../../../../../both/models/todolist.model';
import { Todolists } from '../../../../../both/collections/todolists.collection';

@Injectable()
export class TodolistService {

  todolistSubscription: Subscription;
  todoLists$: ObservableCursor<Todolist> = Todolists.find({});
  numLists$: Observable<number>;
  numLists: number = 0;
  subReady: Subject<boolean> = new Subject<boolean>();

  init(): void {
    if(!this.todolistSubscription){
      this.subReady.startWith(false);
      this.todolistSubscription =     MeteorObservable.subscribe("todolists").subscribe(() => {
        this.todoLists$ = Todolists.find({});
        this.numLists$ = this.todoLists$.collectionCount();
        this.numLists$.subscribe((numberOfLists) => {
          console.log(numberOfLists)
        });
        this.todoLists$.subscribe(() => {
          this.subReady.next(true);
        });
      });      
    }
  }

  isSubscriptionReady(): Subject<boolean> {
    return this.subReady;
  }

  getLists(selector?, options?): ObservableCursor<Todolist> {
    return this.todoLists$;
  }

  getListsCount(): Observable<number> {
    return this.numLists$;
  }

  addList(name: string, description: string): Observable<string> {
    return MeteorObservable.call<string>("addTodoList", name, description);
  }

  addTask(listId: string, identifier: string, description: string, priority: number, start: Date, end: Date): Observable<number> {
    return MeteorObservable.call<number>("addTask", listId, identifier, description, priority, start, end);
  }

  markTask(listId: string, task: Task, index: number) : Observable<number> {
    return MeteorObservable.call<number>("markTask", listId, task, index);
  }

  disposeSubscription() : void {
    if (this.todolistSubscription) {
      this.subReady.next(false);
      this.todolistSubscription.unsubscribe();
      this.todolistSubscription = null;
    }
  }

}

dashboard.component.ts:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { routerTransition } from '../shared/animations'
import { Observable, Subject } from 'rxjs';
import { ObservableCursor } from 'meteor-rxjs';
import { Todolist } from '../../../../both/models/todolist.model';
import { TodolistService } from '../shared/services/todolist.service';

import template from './dashboard.component.html';
import style from './dashboard.component.scss';

@Component({
  selector: 'dashboard',
  template,
  styles: [style],
  animations: [routerTransition()]
})

export class DashboardComponent implements OnInit, OnDestroy {

  todoLists$: ObservableCursor<Todolist>;
  numTodoLists$: Observable<number>;
  numTodoLists: number = 0;

  constructor(private _router: Router, private todolistService: TodolistService) {}

  ngOnInit() {
    this.todolistService.init();
    this.todolistService.isSubscriptionReady().subscribe((isReady) => {
      if(isReady){
        this.todolistService.getListsCount().subscribe((numTodoLists) => {
          this.numTodoLists = numTodoLists;
        });            
      }
    });
  }

  sideNavShown: boolean = true;
  toggleSideNav() {
    this.sideNavShown = !this.sideNavShown;
  }

  ngOnDestroy() {
    this.todolistService.disposeSubscription();
  }

}

dashboard.component.html:

After subscribing to the Observable returned from the service and receiving the value, I assign the value to a variable and use it like that:

<span class="badge badge-accent pull-right">{{ numTodoLists }}</span>

which results in

the number of lists displayed

Also, the value is automatically updated, as soon as I add a new list - everything working as expected.

Thanks SO and especially @ghybs, you are awesome.

1

There are 1 best solutions below

1
ghybs On BEST ANSWER

I noticed that an ObservableCursor (as returned by myCollection.find()) needs to be subscribed to before having any effect. I guess we describe it as being a Cold Observable.

In simple situations (like passing directly the cursor to a template through the AsyncPipe), Angular does the subscription on its own (as part of the async pipe process).

So in your case, you simply need to get a reference to the intermediate object returned by the find(), before you apply collectionCount() on it, so that you can subscribe to it:

const cursor = Todolists.find({});
this.numLists$ = cursor.collectionCount();
this.numLists$.subscribe((numberOfLists) => {
  console.log(numberOfLists);
});
cursor.subscribe(); // The subscribe that makes it work.

Then you can either use numLists$ through AsyncPipe in your template:

{{ numLists$ | async}}

Or you can use a simple intermediate placeholder that you assign within the numLists$.subscribe()

private numLists: number;

// ...
this.numLists$.subscribe((numberOfLists) => {
  this.numLists = numberOfLists;
});

and in your template: {{numLists}}

As for reactivity, you do not need MeteorObservable.autorun() to wrap functions that just re-assign an Observable: the AsyncPipe will properly use the Observable and react accordingly.

The situation is different for findOne(), which does not return an Observable but an object directly.