Skip to content

Documentation Request: provide guide on use of AngularFire in conjunction with @angular/router #624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tarlepp opened this issue Oct 18, 2016 · 21 comments

Comments

@tarlepp
Copy link

tarlepp commented Oct 18, 2016

Version info

Angular: 2.0.1

Firebase: 3.5.0

AngularFire: 2.0.0-beta.5

Other (e.g. Node, browser, operating system) (if applicable):

Test case

messages.resolver.ts

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AngularFire, FirebaseListObservable } from 'angularfire2';

@Injectable()
export class MessagesResolver implements Resolve<any> {
  constructor(private angularFire: AngularFire) { }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ) {
    return this.angularFire.database.list(
      '/messages',
      {
        query: {
          limitToLast: 100
        }
      }
    );
  }
}

room.routing.ts

import { Routes } from '@angular/router';

import { RoomComponent } from './room.component';
import { MessagesResolver } from './messages.resolver';

export const RoomRoutes: Routes = [
  {
    path: 'chat',
    component: RoomComponent,
    resolve: {
      messages: MessagesResolver,
    },
  },
];

room.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FirebaseListObservable } from 'angularfire2';

@Component({
  selector: 'app-chat-room',
  templateUrl: './room.component.html',
  styleUrls: ['./room.component.scss']
})

export class RoomComponent implements OnInit {
  private messages: FirebaseListObservable<any[]>;
  private message: string = '';

  constructor(private activatedRoute: ActivatedRoute) { }

  ngOnInit() {
    this.activatedRoute.data.subscribe(data => {
      this.messages = data['messages'];
    });
  }

  addNewMessage() {
    this.messages.push({
      message: this.message,
      createdAt: firebase.database.ServerValue.TIMESTAMP,
    });

    this.message = '';
  }
}

Debug output

** Errors in the JavaScript console **

** Output from firebase.database().enableLogging(true); **

** Screenshots **

Steps to reproduce

Expected behavior

It should resolve on component ngInit

Actual behavior

Route is not activated at all.

@katowulf
Copy link
Contributor

Hey @tarlepp, some of my rxjs and A2/router newbness is going to show through here, so bear with me. I_think_ this isn't returning a promise, since angularFire.database.list is actually returning an Observable:

return this.angularFire.database.list(...)

This document seems to confirm my naïve assessment. And I think the correct answer here is that you'll want to try one of the techniques described there to get a promise.

However, this feels a bit of an XY problem. Generally speaking, the whole idea here is to let AngularFire2 manage the syncing of the data and not worry about state. When you start managing load state, you've generally started trying to force the realtime streaming into a CRUD model, which leads to a lot of complexities like these.

As a general rule of responsive web apps, you should get the UI in front of users quickly, rather than waiting for all the data to load before you even trigger the routing rule. The data will load in magically as it is downloaded.

☼, Kato

@tarlepp
Copy link
Author

tarlepp commented Oct 19, 2016

@katowulf actually ng2 route resolves uses Observables or Promises. https://angular.io/docs/ts/latest/api/router/index/Resolve-interface.html

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<any>|Promise<any>|any

@katowulf
Copy link
Contributor

katowulf commented Nov 3, 2016

(Again, bear with me, because most of this is new territory and I'm still getting up to speed)

Spent some time on this today. While I haven't resolved it elegantly yet (I'll work on this more), I did get it working using something like the following:

import { Injectable }             from '@angular/core';
import {Router, Resolve, ActivatedRouteSnapshot} from '@angular/router';
import { AngularFire, FirebaseObjectObservable } from 'angularfire2';
import {Observable} from "rxjs";

@Injectable()
export class DetailResolver implements Resolve<FirebaseObjectObservable<any>> {
  constructor(private af: AngularFire) {}
  resolve(route: ActivatedRouteSnapshot): Promise<FirebaseObjectObservable<any>> {
    return Promise.resolve(this.af.database.object('foo'));
  }
}

I couldn't get it to work by returning the observable in resolve(), but by wrapping it in a Promise, I was able to get that working.

I also noticed a related issue with using ngOnInit(). When I didn't declare this.data within my constructor, but waited until ngOnInit() to declare it, nothing would print to the page. However, if I declared anything in the constructor (e.g. this.data = {}) and then reassigned it in my ngOnInit(), then things displayed fine.

Again, still working on a more elegant answer for using resolve, which I'll run past some of the A2 gurus before I present it back here. But some of this may get you started.

@katowulf
Copy link
Contributor

katowulf commented Nov 8, 2016

@jeffbcross @davideast So I've been playing with promises in routers and it's a bit of a mess.

Here are some of the difficulties:

  • toPromise() does not work on our observables, maybe because it's based on complete, which we never invoke?
  • Can do a first() as a workaround, but it's annoying and bloated
  • Resolve method takes an Observable or Promise, but for some reason ours doesn't work here (presumably the same reason that toPromise() doesn't work)

I don't see a great use case for trying to load either of our observables in resolve as opposed to just letting the data load dynamically, but based on history, this will continue to be a common request.

So I think we should probably support a loaded() method or some resolvable promise on FirebaseListObservable and FirebaseObjectObservable, as well as providing a similar way to obtain a resolvable promise on auth state.

@katowulf
Copy link
Contributor

katowulf commented Nov 8, 2016

@tarlepp here's a working master/detail view using routing and resolve (it resolves the master list before routing): https://gist.github.com/katowulf/c493e19a932cd608b811907547be7a78

Some of the key points:

1) FirebaseListObservable and FirebaseObjectObservable never resolve.

You can't just return the Firebase*Observable in your resolve method because they never complete, and Observable.prototype.toPromise() seems to rely on the complete callback. Instead, you need to create your own promise with something like:

let list = this.af.database.list('path/to/data');
return new Promise((resolve, reject) => {
      // must import 'rxjs/add/operator/first'; before using it here
      list.first().subscribe(() => {
          resolve(list)
      }, reject);
});

2) You can't try to establish your Firebase*Observable references in ngOnInit()

Not sure on the cause for this, but this.books (and this.book in the detail view) must be declared in constructor; when moved into ngOnInit() they never get rendered, regardless of what magic I tried here.

@tarlepp
Copy link
Author

tarlepp commented Nov 8, 2016

@katowulf hmm, it seems to work if I use your example of that resolve implementation. But still I'm a bit confused why returning observable doesn't work like it should.

@katowulf
Copy link
Contributor

katowulf commented Nov 8, 2016

I think the primary issue is #1: FirebaseListObservable and FirebaseObjectObservable don't ever "finish" (i.e. call onCompleted) like a normal observables (just call onProgress and onError), so toPromise() never resolves.

@tarlepp
Copy link
Author

tarlepp commented Nov 9, 2016

Hmm so this is a real bug then.

@katowulf
Copy link
Contributor

katowulf commented Nov 9, 2016

Well, not precisely. We're observing an event stream. It doesn't resolve; it's always in progress.

As I mentioned before, waiting on "finished" data, etc, isn't really an appropriate way to think about Firebase (it's trying to turn the real-time aspects into a CRUD strategy).

While these two ideas aren't exactly incompatible, the onComplete handler, and by extension, the toPromise method won't ever be invoked (nor should they), making it difficult to use a Firebase observable in the same way you would a more traditional, short-lived Observable subscription.

@tarlepp
Copy link
Author

tarlepp commented Nov 9, 2016

Hmm, and I'm just more confused :D

If Angular2 resolves can return a promise or observable as in the docs https://angular.io/docs/ts/latest/api/router/index/Resolve-interface.html and this.af.database.list('path/to/data') returns a observable that should be ok - right?

So which one has the bug then?

  1. angular2 route resolve handling with observables
  2. angularfire2 observable
  3. or something else

@katowulf
Copy link
Contributor

katowulf commented Nov 9, 2016

It's an event stream. It's never resolved. It keeps waiting for more events. You aren't downloading a static data model, you're subscribing to a continuous event stream. So none are wrong, it's just a slightly different paradigm that doesn't fit perfectly with a promise strategy.

@BirdInTheCity
Copy link

+1 for finding a cleaner solution.

The Firebase paradigm of continuous event stream makes sense, but it's a pretty common use case to have initial data loaded prior to resolving a route. I'll be curious to see if any other ideas develop.

@Benny739
Copy link

Benny739 commented Dec 21, 2016

`resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
let id = route.params['id'];

    return Observable.from(this.ss.getSupplierById(id))
            .do(s => {
                if (s.$value === null){
                    this.router.navigate(['suppliers']);
                    return null;
                }
                return s;
            }).take(1);
}`

This is working fine.

@denizcoskun
Copy link

here is the full example.

`
import { Injectable } from '@angular/core';
import { AngularFire, FirebaseObjectObservable } from 'angularfire2';
import { AuthService } from './auth';
import { Router, Resolve, RouterStateSnapshot,
ActivatedRouteSnapshot } from '@angular/router';
import { Folder } from './models/folder.model';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/first';

@Injectable()
export class FolderResolver implements Resolve<FirebaseObjectObservable> {

constructor(private af: AngularFire, private auth: AuthService, private router: Router) {
}

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FirebaseObjectObservable> {

let id = route.params['id'];
console.log(id);

let folderID = `/users/${this.auth.id}/folders/${id}`;
let folder =  this.af.database.object(folderID);

return folder.do((s) => {
    if(s.$value === null) {
        this.router.navigate(['/dashboard']);
        return null;
    } else {
        return folder;
    }
}).take(1);

}`

@bogacg
Copy link

bogacg commented Feb 20, 2017

Doesn't .first() or .take(1) stops further reading of updates? With this, if data gets updated, we won't see any effect in our app I expect. Also, proposed solutions aren't really intuitive.

@myspivey
Copy link
Contributor

Subscribing to this as well. A common practice for us to resolve certain pieces of data for a view.

@leblancmeneses
Copy link

I'm looking for a way to convert angularfire
$firebaseAuth().$waitForSignIn()
$firebaseAuth().$requireSignIn()
to angularfire2.

example route resolve usecases:

  1. /shared/:userId resolve $requireSignIn() and determine if auth.uid has access to userid profile.
  2. $firebaseAuth().$waitForSignIn() ensures loaded view can synchronously access auth value with const firebaseUser = $scope.authObj.$getAuth();

@mrdowden
Copy link

mrdowden commented Sep 11, 2017

@leblancmeneses I'm also looking for a replacement for $firebaseAuth().$requireSignIn() as this was one of the most powerful features of AngularFire IMO.

UPDATE: Actually @martine-dowden found a way to do this using the AngularFireAuth: angularFireAuth.authState.take(1) returns an Observable that works correctly when returned from a Resolver.

@leblancmeneses
Copy link

@mrdowden here is what I am using.

RequireSignInGuard:

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFireAuth } from 'angularfire2/auth';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class RequireSignInGuard implements CanActivate {

  constructor(private fbAuth: AngularFireAuth, private router: Router) {
  }

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    return this.fbAuth.authState.map((auth) => {
       if (!auth) {
         this.router.navigateByUrl('login');
         return false;
       }
       return true;
    }).take(1);
  }
}

WaitForSignInGuard:

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AngularFireAuth } from 'angularfire2/auth';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class WaitForSignInGuard implements CanActivate {

  constructor(private fbAuth: AngularFireAuth, private router: Router) {
  }

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    return this.fbAuth.authState.map((auth) => {
       return true;
    }).take(1);
  }
}

@escobara
Copy link

The problem with using first() is that it will only call the object or list then automatically unsubscribe which will cause problems when reading live changes on the page. So you would need to reload the page if you want to see the changes. I ran into this problem when trying to load a object so that I could edit it.

@jamesdaniels jamesdaniels changed the title Docs how use within Route resolve Docs: No documentation on how user AngularFire in conjunction with @angular/router May 17, 2018
@jamesdaniels jamesdaniels changed the title Docs: No documentation on how user AngularFire in conjunction with @angular/router Docs: No documentation on how use AngularFire in conjunction with @angular/router May 17, 2018
@jamesdaniels jamesdaniels changed the title Docs: No documentation on how use AngularFire in conjunction with @angular/router Documentation Request: provide guide on use of AngularFire in conjunction with @angular/router May 18, 2018
@jamesdaniels
Copy link
Member

Closing as outdated, we could do more work here but I think there's plenty of samples/content floating around now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests