In my Angular service I am providing a public Observable called usageDoc$.
service.ts:
usageDoc$: Observable<IUsage>;
initializeUsageDoc() {
//gets called in app.component.ts on app load
...
//some async db querying
this.usageDoc$ = this.firestore.getUsageDoc(user.uid); //getUsageDoc(..) returns Observable<IUsage>
}
component.ts
localDoc: any;
ngOnInit() {
this.service.usageDoc$.subscribe(doc=>this.localDoc=doc);
}
This leads to the following error: cannot read property subscribe of undefined... as usageDoc$ is not yet set on the component init. Currently I am using a workaround by creating a second observable usageDocReady$ = new Subject<boolean> in service that emits as soon as usageDoc$ is set.
Is there better to solve this issue? Can I somehow initialize usageDoc$ with a default value?
I know I would not have this issue if I'd subscribe in template using the async pipe, but I need a normal variable for displaying matters, hence the use of localDoc.
I'd suggest using a ReplaySubject with buffer 1. It'd ensure future subscribers would get the last emission whilst avoiding any undefined errors.
private usageDocSrc = new ReplaySubject<IUsage>(1);
public usageDoc$ = this.usageDocSrc.asObservable();
initializeUsageDoc() {
//gets called in app.component.ts on app load
...
//some async db querying
this.firestore.getUsageDoc(user.uid).subscribe({
next: value => this.usageDocSrc.next(value)
});
}
If you don't wish to use a ReplaySubject, then I don't understand the need for the Observable. You could directly return the observable from getUsageDoc() function.
initializeUsageDoc(): Observable<IUsage> {
//gets called in app.component.ts on app load
...
//some async db querying
return this.firestore.getUsageDoc(user.uid);
}
Although this would invoke all the statements prior to getUsageDoc() for each subscription to initializeUsageDoc().
Related
Consider the code:
res: any;
getData(url: any) {
this.res = this.http.get(url);
}
ngOnInit(): void {
getData("url.com/file.json");
console.log(this.res)
}
In the console I get
Observable {source: Observable, operator: ƒ}
How do I get the contents of the json file instead of an observable?
First, the way you define the property and use the httpClient verbs in unconventional. If you want to assign the Http verb (i.e GET) to a property this property will hold an Observable, because HttpClient verbs return Observables in Angular and they are powered by RxJS under the hood.
Second, with this being said, if you want to get into the JSON part you need to subscribe to the Observable i.e listen to the values that will be broadcasted by the Observer.
You have two ways to do that;
1 - As per Andrew description by assinging the JSON to the property within the subscription.
2- You keep the property assigned an Observable and use async pipe in your template.
Now, why you are getting undefined ? because during initialization of the component your asynchronous data has not been assinged yet to your property. Here, you have multiple options, including using a different lifecycle hook such as ngAfterViewInit or use a guard condition to check
if(res) { //stuff}
In all cases, you need a small refactoring for the unconventional way of handling Http calls as in your code snippets above.
You need to subscribe to the observable.
getData(url: any) {
this.http.get(url).subscribe(result => { this.res = result; });
}
Few things before hand :
This is going to be long and precise cause I really want to make my problem clear
I've read the RXJS docs
I've seen this Wrap an API function in an RxJs Observable post.
I've seen several other posts.
none, unfortunately, helped me out.
I'm writing in Vue 2.6
I've created a wrapper to easily manage RXJS in my Vue application
I've instantiated an RXJS instance as a singletone object in my application
Vue.prototype.$rx = Rxjs;
I've also stored this instance of my RXJS in my VUEX
store.commit('setProperty', {key: 'rx', value: Vue.prototype.$rx})
So that I can utilise RXJS in my VUEX store
my RXJS object is just a wrapper
export const Rxjs = {
create(namespace, subjectType = 'subject') {
this[namespace] = new RxjsInstance(subjectType)
}
}
which instantiates a new RXJS class for each namespace in my application based on my VUEX namespaced modules
So for a "Homepage" screen I've got both a Homepage VUEX module an now an RXJS (if is needed - they do not instantiate automatically) instance to hold homepage's subscriptions
based on this class:
export class RxjsInstance {
constructor(subjectType) {
this.subscriptions = new Subscription();
switch (subjectType) {
case 'subject':
this.subject = new Subject();
break;
case 'asyncSubject':
this.subject = new AsyncSubject();
break;
case 'behaviorSubject':
this.subject = new BehaviorSubject(null);
break;
case 'replaySubject':
this.subject = new ReplaySubject();
break;
}
}
next(data) {
this.subject.next(data);
}
asObservable() {
return this.subject.asObservable();
}
set add(subscription) {
this.subscriptions.add(subscription);
}
subscribe(subscriber, callback) {
if (!subscriber) {
throw 'Failed to subscribe. Please provide a subscriber'
}
subscriber = this.asObservable().subscribe(data => {
callback(data)
});
this.subscriptions.add(subscriber)
}
unsubscribe() {
this.subscriptions.unsubscribe();
}
}
In short - this class literally returns a barebone RXJS functionality with the added value of having the subscriptions automatically saved in a local array of the instance for easier control and prevention of memory leaks - nothing fancy - just simplification for my team.
So as it stands I've got a gloal RXJS object that sits everywhere in my application - that I can access from every component and allows me to transmit data from point A to point B without having to use several 'props' and an equal amount of emits between parent and child component.
all that without touching my VUEX state (which unlike Redux is not an immutable)
So that when I do mutate my state - Its clean and controlled data.
NOW for my problem.
I'm trying to figure out a way to use RXJS to wrap an existing function with an observable so that I can subscribe to it whenever the function is called.
Lets say I've got a alertUser(msg) function which just appends the 'msg' arg to a element in my DOM.
I'd like to 'subscribe' to this function - without changing it at all! by wrapping it with an observable or assigning one to it.
I'm not sure that's doable - because all I've seen RXJS doing is creating an observable that performs the function and then subbing to it.
but because I'm integrating RXJS into an already existing application - I can't refactor everything to an observable.
So my end goal is
alertUser(msg){
this.appendToPElement(msg)
}
this.localVariable = ObservThatFunction(){
\\ this is where I want the magic to happen and listen to the alertMsg func and make this an observable i can subscribe to
}
this.sub = this.localvariable.subscribe(data=>{console.log(data)})
is this even possible?
EDIT:
the observable doesn't have to pass the function's data - just emit a value (even a bool value) when the function is called
so for example when this.alertUser() is called i want the observable to called - and my subscriber can now update the notification service that a new msg has been sent - regardless of what that message is
TL;DR: It is simply impossible.
In short, you want to spy over a particular method. The only way to reliably spy/stub in JavaScript is using ES Proxy. But proxy is like a wrapper around a method. If this function is part of object which you are exporting, then Proxy would have worked. But that is not the case. Also, you need to call the proxied function and not the original one directly. Another way is to use JavaScript [Decorators][2] but they are not yet standard and useful in the class/method context only.
But, as a workaround, you can modify the function without changing the semantics and wrapping it into observable. The idea is to create a global Subject and call it from the alertUser():
window.messageQueue = new Subject();
alertUser(msg){
this.appendToPElement(msg);
// This is non-destructive.
window.messageQueue.next(msg);
}
// Usage
window.messageQueue.subscribe(() => { /* next */});
Just guessing here, but maybe a Proxy could be helpful.
const handler = {
apply: function(target, thisArg, argumentsList) {
obs.next(true);
return target(argumentsList);
}
};
function observeFn(fn) {
return new Proxy(fn, handler);
}
Then you could just wrap your original function like this: observeFn(alertUser) and obs should notify any call to that function.
I'm using Angular 2 and I have noticed an unexpected behaviour.
I have a datasource class, which extends DataSource, with two variables:
private archives = new BehaviorSubject<MyArchive[]>([]);
public archives$: Observable<MyArchive[]> = this.archives.asObservable();
private filteredArchive: MyArchive[];
I update archives this way within the class:
this.archives.next(this.filteredArchive);
Outside in another class I try to subscribe to the observable but it doesn't work:
ngAfterViewInit(): void {
this.dataSource.archives$.subscribe((archive: Array<MyArchive>) => {
console.log(archive.length);
console.log(archive);
console.log(archive.length);
}
}
In the console log it prints:
0
<THE ARCHIVE WITH AN OBJECT INSIDE AS IT SHOULD BE, WITH LENGTH = 1>
0
So that I can't iterate on the archive variable because its length is 0. What's going on here?
the issue this the value is already emitted before you subscribe. Think of observables like tv stream and your data is like a show on this stream if you open the tv after the show ended (subscribe after you pushed the data) you never see the show. if you want your observable to keep the last value you can use the Scan operator like this :
export const reducer = () =>
scan<MyArchive[]>((archive, update) => {
//update your Archive or simply overwrite the value
archive = update
return archive
}, []);
export class datasource {
archives$ : Observable<MyArchive[]>;
archives : Subject<MyArchive[]> = new Subject([]);
update(newMyArchive: MyArchive[]) {
this.archives.next(newMyArchive);
}
constructor(public http: HttpClient) {
this.archives$ = this.archives.pipe(
reducer(),
//use the shareReplay so all your subscribers get the same values
shareReplay(1)
);
this.archives$.subscribe();
}
and you can update the Arcjive useing the update method in the datasource class like:
this.update(filteredArchive)
I'm not sure why you're redundantly creating archives$ and archives. You can do it with just a single BehaviorSubject.
I think you should be doing this:
// Create a BehaviorSubject for MyArchive[]
public archives$: BehaviorSubject<MyArchive[]> = new BehaviorSubject<MyArchive[]>([]);
// Create a private filtered Array for MyArchive.
private filteredArchive: MyArchive[];
And then,
this.archives$.next(this.filteredArchive);
When you use this statement:
console.log(archive);
a reference to the archive object is shown in the console (see the MDN documentation). The content of the object can be different when you inspect it in the console (by clicking on the arrow) from what it was when the console.log statement was executed.
In order to see what the actual content is at a specific moment, you should output a string representation of the object:
console.log(archive.toString());
console.log(JSON.stringify(archive));
You can expermiment with this stackblitz to see the various console outputs.
I solved the problem.
I forgot that this.filteredArchive is updated by an HTTP GET request. I was trying to push data within this.filteredArchive using forEach, but I forgot to call then() when all the data are received.
Hence I was trying to access, using subscribe, a result that was not yet ready.
Thanks everyone for the replies!
In my angular project I currently have a service that uses http calls to retrieve data from my java code. Every 10 seconds the service calls the Java side and gets new data from it. I now have another component that needs the data in my service. I need this component to have a field called 'data' that just gets updated automatically when the service gets new information.
How can I set them up so that the service pushes the new information to my other component? I would like to be able to use {{data}} in my component's html and have that be automatically updated without having to reload the page.
My component does have the service 'Autowired' in already. So currently I can just call 'this.data = this.service.getData()' but that call is within my ngOnInit method so it only happens once, and the data field does not get updated when the service's data field gets updated.
You can create a messaging service that publishes data for subscribers or implement this functionality in your original service.
I would suggest having a separate messaging service and have relevant components or services publish/subscribe to it.
messaging.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs/Subject';
#Injectable()
export class MessagingService {
private sharedValue = new Subject<string>();
// Observable string streams
sharedValue$ = this.sharedValue.asObservable();
// Service message commands
publishData(data: string) {
this.sharedValue.next(data);
}
You would inject the service like this:
constructor(private messagingService: MessagingService ) {}
Publish to the service:
this.messagingService.publishData(sharedValue);
Subscribe to the service:
this.messagingService.sharedValue$.subscribe(
data => {
this.localSharedValue = data;
});
FROM ANSWER BELOW BY: DeborahK (who's courses on Pluralsight everyone should watch)
Actually, all you need is a getter.
Change your 'data' property to a getter and that will do the trick:
get data(): any {
return this.service.getData();
}
Angular change detection will detect any time that data is changed in
the service which will cause it to re-evaluate its bindings and call
this getter to re-get the data.
No need for a fancy service or Subject. :-)
Actually, all you need is a getter.
Change your 'data' property to a getter and that will do the trick:
get data(): any {
return this.service.getData();
}
Angular change detection will detect any time that data is changed in the service which will cause it to re-evaluate its bindings and call this getter to re-get the data.
No need for a fancy service or Subject. :-)
I know that by definition when an Observable is Completed or it has an Error it became cold and you cannot pass data to it.
So if you want to pass data after the Error you need to create a new Observable.
I'm just wondering if you can somehow restore the Observable after the Error, as it makes more sense for me to restore it instead to create a new one with the same functionality ?
Ex :
const subject = new Rx.Subject();
subject.subscribe(
data => console.log(data),
error => console.log('error')
);
subject.next('new data');
subject.next('new data2');
subject.error('error');
subject.next('new data3');
The new data3 is not sent as it has an error before that.
Working example : https://jsbin.com/sizinovude/edit?js,console
PS : I want the subscriber to have the error and also send data after, looks like the only way is to create a new Observable and subscribe to the new one after the error.
No, my understanding is that you cannot emit further data to subscribers after the observable errors.
The Observable Termination section in the Observable Contract states:
When an Observable does issue an OnCompleted or OnError notification, the Observable may release its resources and terminate, and its observers should not attempt to communicate with it any further.
This suggests to me that if you call next after error, any subscribers will not receive the value, as they will have unsubscribed upon receipt of the error notification. This is also mentioned in the Observable Termination section:
When an Observable issues an OnError or OnComplete notification to its observers, this ends the subscription.
One strategy could be to handle well know errors (i.e. data validation) in observables by creating an observable that is the error stream.
I use .NET but I am sure you get the idea:
class UseSubject
{
public class Order
{
private DateTime? _paidDate;
private readonly Subject<Order> _paidSubj = new Subject<Order>();
private readonly Subject<Error> _errorSubj = new Subject<Error>();
public IObservable<Order> Paid { get { return _paidSubj.AsObservable(); } }
public IObserble<Error> Error {get {return _errorSubj.AsObservable();
}}
public void MarkPaid(DateTime paidDate)
{
if (!paidDate.isValid){_errorSubj.OnNext(New Error("invalid paidDate")); return}
_paidDate = paidDate;
_paidSubj.OnNext(this); // Raise PAID event
}
}
private static void Main()
{
var order = new Order();
order.Paid.Subscribe(_ => Console.WriteLine("Paid")); // Subscribe
order.Error.Subscribe(_ => Console.WriteLine("Error"));
order.MarkPaid(DateTime.Now);
}
}