7

I am building an angular 2 application. The documentation has changed quite a bit since the released which has caused confusion. The best I can do is explain what I am trying to do (Which was easy in Angular 1) and hope someone can help me out.

I have created a login service using JWT's. Once login is successful, I return a user object.

I have a loginComponent ( binds data to template ) and loginService ( which handles the https calls )

I have a userService which maintains the user object.

I have a userComponent which renders the user data.

The problem is, once the user has logged in, I am unclear on the best approach for letting the userService retrieve the new data in an object called "user", then the userComponent update its user object on the template. This was easy in angular 1 simply by putting a watcher on the userService.user object.

I tried Inputs and Outputs to no avail, eventEmitters, Observables and getters and setters. The getters and setters work, but force me to store everything in a "val()"

Can someone please tell me the best way to achieve this?

  1. User Component renders template with user.firstName, user.lastName etc.
  2. Initially user if an empty Object
  3. The login service needs to set the UserService.user
  4. The userComponent Needs to detect the change and update the DOM.

Thanks in ADVANCE!

Judson Terrell
  • 4,204
  • 2
  • 29
  • 45

2 Answers2

9

If I'm not wrong, you are looking for a way to 'listen' to changes in your UserService.user to make appropriate updates in your UserComponent. It is fairly easy to do that with Subject (or BehaviorSubject).

-In your UserService, declare a property user with type Subject<User>.

user: Subject<User> = new Subject();

-Expose it to outside as observable:

user$: Observable<User>
...
this.user$ = this.user.asObservable();

-Login function will update the private user Subject.

login(userName: string, password: string) {
  //...
  this.user.next(new User("First name", "Last name"));
}

-In your UserComponent, subscribe to UserServive's user$ observable to update view.

this.userService.user$.subscribe((userData) => {this.user = userData;});

-In your view, simply use string interpolation:

{{user?.firstName}} {{user?.lastName}}

Here is the working plunker: http://plnkr.co/edit/qUR0spZL9hgZkBe8PHw4?p=preview

Harry Ninh
  • 16,288
  • 5
  • 60
  • 54
  • So essentially this works the same as watchers in angular 1? The view gets updated when data changed in the service? – Judson Terrell Aug 13 '16 at 02:49
  • Also, what us the purpose of the ? marks? optional variables? – Judson Terrell Aug 13 '16 at 02:49
  • Yes. You can play around with it in the provided plunker. The existential operator (`?.`) is used to safely retrieve properties value of a nullable object. In case `user` is null or undefined, this will gently render empty string to the mark up, while using `user.firstName` will throw you some error. In the plunker, I wrap everything in a `ngIf` so I don't need that. – Harry Ninh Aug 13 '16 at 02:52
  • The data in service is updated in such a way that enables others to subscribe to its changes (`Subject.next`). We listen to that event by calling `user$.subscribe` in any component that's interested in it. – Harry Ninh Aug 13 '16 at 02:54
  • Im initially binding my model data like so... ****How do I make the binding optional so that the user object doesnt fail if it doesnt exist yet? I added user = {} to the constructor. Is this correct way? – Judson Terrell Aug 13 '16 at 03:00
  • I Fixed it llike this... user: any; constructor(private userService: UserService) { this.user = {}; this.userService.user$.subscribe((userData) => { this.user = userData; }); } – Judson Terrell Aug 13 '16 at 03:02
  • Last thing, what training sources did you use to determine that Subject is better than eventEmitter etc.? – Judson Terrell Aug 13 '16 at 03:03
  • Hey, you can find more materials regarding Reactive Programming for this matter. Basically you'd need to understand the core concept of Observable, Observer, different types of Subjects and different types of operators over them (like `map`, `switchMap`, etc). `EventEmitter` is, in fact, a subclass of `Subject` (https://github.com/angular/angular/blob/master/modules/%40angular/facade/src/async.ts) that is intended for different purpose; so technically you can use it here, but I'd rather not to. – Harry Ninh Aug 13 '16 at 03:10
  • One more thing Im getting an error... this.user$ = this.user.asObservable(); ERROR: Assigned Expression Type Observable is not assignable to type Observable END.... Any ideas? – Judson Terrell Aug 13 '16 at 03:17
  • Harry, I will give you access to it on github can you provide me with your email or something so I can give you access to my private repo? you can message me at judsonmusic@me.com – Judson Terrell Aug 13 '16 at 05:03
4

There are two rather different approaches you could take:

1. Share data via JavaScript reference types

If you create an object in your UserService

@Injectable()
export class UserService {
   public user = new User();

you can then share that object just by virtue of it being a JavaScript reference type. Any other service or component that injects the UserService will have access to that user object. As long as you only modify the original object (i.e., you don't assign a new object) in your service,

   updateUser(user:User) {
       this.user.firstName = user.firstName;
       this.user.lastName  = user.lastName;
   }

all of your views will automatically update and show the new data after it is changed (because of the way Angular change detection works). There is no need for any Angular 1-like watchers.

Here's an example plunker.

In the plunker, instead of a shared user object, it has a shared data object. There is a change data button that you can click that will call a changeData() method on the service. You can see that the AppComponent's view automatically updates when the service changes its data property. You don't have to write any code to make this work -- no getter, setter, Input, Output/EventEmitter, or Observable is required.

The view update automatically happens because (by default) Angular change detection checks all of the template bindings (like {{data.prop1}}) each time a monkey-patched asynchronous event fires (such as a button click).

2. "Push" data using RxJS

@HarryNinh covered this pretty well in his answer. See also Cookbook topic Parent and children communicate via a service. It shows how to use a Subject to facilitate communications "within a family".

I would suggest using a BehaviorSubject instead of a Subject because a BehaviorSubject has the notion of "the current value", which is likely applicable here. Consider, if you use routing and (based on some user action) you move to a new route and create a new component, you might want that new component to be able check the "current value" of the user. You'll need a BehaviorSubject to make that work. If you use a regular Subject, the new component will have no way to retrieve the current value, since subscribers to a Subject can only get newly emitted values.


So, should we use approach 1. or 2.? As usual, "it depends". Approach 1. is a lot less code, and you don't need to understand RxJS (but you do need to understand JavaScript reference types). Approach 2. is all the rage these days.

Approach 2. could also be more efficient than 1., but because Angular's default change detection strategy is to "check all components", you would need to use the OnPush change detection strategy and markForCheck() (I'm not going to get into how to use those here) to make it more efficient than approach 1.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Hi can you please check this? https://stackoverflow.com/questions/48355599/loading-large-array-in-oi-select-takes-too-much-of-time-in-angularjs – Sudarshan Kalebere Jan 24 '18 at 10:41