Closed tarlepp closed 3 years ago
Hey @tarlepp, some of my rxjs and A2/router newbness is going to show through here, so bear with me. Ithink 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
@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
(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.
@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?first()
as a workaround, but it's annoying and bloatedtoPromise()
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.
@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 FirebaseObservable 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.
@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.
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.
Hmm so this is a real bug then.
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.
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
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.
+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.
`resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable
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.
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);
}`
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.
Subscribing to this as well. A common practice for us to resolve certain pieces of data for a view.
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();
@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.
@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);
}
}
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.
Closing as outdated, we could do more work here but I think there's plenty of samples/content floating around now.
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
room.routing.ts
room.component.ts
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.