93

I'm trying to return an observable when I get a certain value in a subscriber, but I fail miserably.

This is the code:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
    // get route to be activated
    this.routeToActivate = route.routeConfig.path;

    // get user access levels        
    return this._firebase.isUserAdmin          <-- returns Subscription, not Observable
        .map(user => user.access_level)
        .subscribe( access => {
           // I need to return an observable here
        });
}

There are not many resources on observables in angular 2, so I don't know where to start. Can anyone help with this please?

UPDATE -> Working Version

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
            // get route to be activated
            this.routeToActivate = route.routeConfig.path;

            // get user access levels        
            return this._firebase.isUserAdmin
                .map(user => {
                    let accessLevel = user.access_level;

                    if (accessLevel === 'admin' ) {
                        return true;
                    }

                }).first();
        }
laike9m
  • 18,344
  • 20
  • 107
  • 140
Dragos Ionescu
  • 1,163
  • 1
  • 9
  • 12
  • 1
    You should be googling Observables in **rxjs** rather than ng2. there are tons of resources – drew moore Oct 08 '16 at 18:16
  • Thank you! I tried that but they don't come in the same format Angular 2 uses them. I guess I will have to learn that from scratch then. – Dragos Ionescu Oct 08 '16 at 18:35
  • Can you explain the context for this code further? Since `subscribe` returns `Subscription` object to unsubscribe it and thus not chainable, generally you want to do `observable.subscribe(...); return observable` . – Estus Flask Oct 08 '16 at 18:38
  • 3
    Angular uses rxjs5. Many resources are about rxjs4. I guess this is what you mean with "don't come in the same format" (https://github.com/ReactiveX/rxjs == V5 vs https://github.com/Reactive-Extensions/RxJS == V4) – Günter Zöchbauer Oct 08 '16 at 18:40

6 Answers6

105

You can't return an observable from subscribe but if you use map instead of subscribe then an Observable is returned.

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
    // get route to be activated
    this.routeToActivate = route.routeConfig.path;

    // get user access levels        
    return this._firebase.isUserAdminObservable
        .map(user => {
           // do something here
           // user.access_level;
           return true;
         })
        .first(); // for the observable to complete on the first event (usually required for `canActivate`)
        // first needs to be imported like `map`, ...
}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
    // get route to be activated
    this.routeToActivate = route.routeConfig.path;

    let subject = new Subject();
    // get user access levels        
    this._firebase.isUserAdminObservable
        .map(user => {
          let accessLevel = user.access_level; 
          if (accessLevel === 'admin' ) { 
            subject.emit(true); 
            subject.complete();
          } 
          return user;
        });
     return subject;
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Hi, thanks for the answer! I need to do some processing inside subscribe depending on the value and then return true|false – Dragos Ionescu Oct 08 '16 at 18:16
  • You can do it as well in `map`, just change `user.access_level` to `{user.access_level; ...}` and you can add multiple statements like in a normal function. Ensure you return the result at the end otherwise the receiver won't receive anything. – Günter Zöchbauer Oct 08 '16 at 18:17
  • I tried using this: `return this._firebase.isUserAdmin .map(user => { let accessLevel = user.access_level; if (accessLevel === 'admin' ) { return true; } }).first();` but it doesn't unlock the view so I guess it doesn't return that true statement. – Dragos Ionescu Oct 08 '16 at 18:23
  • That should just work. You can try to explicitly return `false` after the `if`. When you add a `console.log(accessLevel);` does it print `admin`? – Günter Zöchbauer Oct 08 '16 at 18:27
  • Sorry, there was an error on my part as the accessLevel was just supposed to return 0. It does print admin now, but I'm getting errors from a different component that depends on the isUserAdmin method when I use first(). Is there a way to not end the stream? – Dragos Ionescu Oct 08 '16 at 18:32
  • If the stream doesn't end the router doesn't activate the route. What you can do is to create a new stream that ends when the `user` event arrives. I'll update the answer. – Günter Zöchbauer Oct 08 '16 at 18:34
  • Not sure though if this actually fixes your issue. I don't understand your issue with `first()` and `isUserAdmin`. – Günter Zöchbauer Oct 08 '16 at 18:39
  • I tried it, but apparently .emit() does not exist on Subject. Basically, I have a parent route that first verifies if the user is logged in, and then all the child routes(like this one) are verifying the permissions as different users have different route permissions. The isUserAdmin method is required in other parts of the application to generate dynamic content after the component is initiated, so ending the stream would break that code. – Dragos Ionescu Oct 08 '16 at 18:47
  • Then it's probably `next()`. I mix them always up because `EventEmitter` and `Subject` have different names and I don't actively use TS myself. – Günter Zöchbauer Oct 08 '16 at 18:49
  • 1
    That was it. There are no errors now, but the route still doesn't unlock, although it's sending true. I will try to fiddle with it and get it working, maybe I'll learn something in the process. Thank you very much! – Dragos Ionescu Oct 08 '16 at 18:53
  • Question, why can the function have side effects? I mean, I can update a member variable in a typescript class for example which was rather unexpected. – Anytoe Jan 16 '18 at 21:33
  • @anytoe what function are you referring to? TypeScript has no way to enforce pure functions. – Günter Zöchbauer Jan 17 '18 at 04:27
  • 1
    Sorry, I meant the map function seems to take a lambda which can access variables outside it's scope. But I guess that's what lambdas can do - It kind of surprised me. However, solved my problem! – Anytoe Jan 17 '18 at 07:58
  • 1
    @anytoe right, there is no way to prevent that. You have take care of that yourself. – Günter Zöchbauer Jan 17 '18 at 07:59
  • 4
    @GünterZöchbauer, can you update the answer to include `RxJS 6.0` code? – th1rdey3 Aug 29 '18 at 10:37
11

Couldn't we just use pipe with map from import { map } from 'rxjs/operators';?

import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class SearchHostService {
  constructor(private readonly http: HttpClient) {}

  searchForHosts(searchString: string): Observable<Employee[]> {
    return this.http
      .get<Employee[]>('./assets/host-users.json')
      .pipe(
        map(employees =>
          employees.filter(({ displayName }) =>
            displayName.toLowerCase().startsWith(searchString),
          ),
        ),
      );
  }
}
developer033
  • 24,267
  • 8
  • 82
  • 108
Sibeesh Venu
  • 18,755
  • 12
  • 103
  • 140
  • But what if we arent' returning Observable but a different value? The original question is calling code that gets an Observable but then he wants to return an Observable – Rhyous Sep 27 '21 at 21:55
11

Do this if you want to subscribe to observable, process the result and then return the same result in subscribe:

function xyx(): Observable<any> { 
    const response = someFunctionThatReturnsObservable().pipe(map(result => {
          // here do any processing of the result //
          return result; // return back same result.
       }
    ))
   return response;
}
Zaslam
  • 334
  • 1
  • 3
  • 8
6

We can convert the Observable object to promise using toPromise method. so the code can be implemented as followed:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Promise<boolean> {
        // get route to be activated
        this.routeToActivate = route.routeConfig.path;

        // get user access levels        
        return this._firebase.isUserAdmin
            .map(user => {
                return (user.access_level === 'admin');
            }).toPromise();
    }
Rohit Gupta
  • 1,368
  • 1
  • 14
  • 30
2

You don't need map, code below specifies a predicate and a projection function for first.

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
    this.routeToActivate = route.routeConfig.path;      
    return this._firebase.isUserAdminObservable
        .first((_, index) => index === 0, user => {
           // do something here
           // user.access_level;
           return true;
         })
}

More on first

leoncc
  • 233
  • 3
  • 6
0

You can create a new observable, and fire the event according to the access level.

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
    // get route to be activated
    this.routeToActivate = route.routeConfig.path;

    // get user access levels
    return new Observable(subscriber=>{
       this._firebase.isUserAdmin
        .map(user => user.access_level)
        .subscribe(access => {
           // Return an observable!
           // Change your logic here...
           return access === XXX ? subscriber.next(true) : subscriber.next(false);
        }, err => subscriber.error());
    })
}

Reference: https://rxjs-dev.firebaseapp.com/guide/observable

Orio Ryo
  • 132
  • 1
  • 2
  • 8