Hi i'm building a chat app with angular for a school project i'm using firebase for my backend and i have an issue with my ngfor.
For exemple if i reload the page i will see nothing unless i hover my routerlink on my navbar. However sometime it will work after some time on the page without any action
When i recieve message i need to be on the page to see them ...
When i reload my page in first time my array is empty this may be what makes the ngfor bug
array on reload.
I'm using ngOnInit() to subscribe :
messages: Message[];
messageSubscription: Subscription;
constructor(private messageService: MessageService, private router: Router) {
}
ngOnInit(): void {
this.messageSubscription = this.messageService.messageSubject.subscribe(
(messages: Message[]) => {
console.log(messages)
this.messages = messages;
}
);
this.messageService.getMessage();
this.messageService.emitMessage();
}
ngOnDestroy(): void {
this.messageSubscription.unsubscribe();
}
This is my html template :
<div *ngFor="let message of messages" class="message-box">
<img [src]="message.photoURL" class="profile-picture">
<div class="content-box">
<div class="information">
<p class="username">{{message.displayName}}</p>
<p class="date">{{message.createdAt | date: 'short'}}</p>
</div>
<p class="text">{{message.text}}</p>
</div>
</div>
Here you can find my service with my getMessage() function and emitMessage():
messages:Message[] = [];
messageSubject = new Subject<Message[]>();
constructor() { }
emitMessage(){
this.messageSubject.next(this.messages);
}
saveMessage(newMessage: Message){
firebase.database().ref('/message').push(newMessage);
}
getMessage(){
firebase.database().ref('/message')
.on('value', (data) => {
this.messages = data.val() ? Object.values(data.val()): [];
this.emitMessage();
});
}
And this is the repo of my project: https://github.com/Zuxaw/AngularChatApp
If anyone has a solution I'm interested
Problem is, your firebase library is not Angular specific.
This means you some times need to make sure its code, mostly its event callbacks, run within an Angular zone (google to read about it) to make sure a change detection 'tick' is invoked when data changes.
message.service.ts
import { Injectable, NgZone } from '#angular/core';
// ...
constructor(private zone: NgZone) { }
// ..
getMessage(){
firebase.database().ref('/message')
.on('value', (data) => {
this.zone.run(() => {
this.messages = data.val() ? Object.values(data.val()): [];
this.emitMessage();
});
});
}
I think you might need to use the child_added event instead of value in your getMessage method.
Check if you're receiving data on time in your getMessage method, if not it's most probably, because of the event.
But one thing that I don't understand is why you're calling emitMessage inside getMessage and also calling it inside your component after getMessage, try to evade that.
Related
I have a div and need to add attribute depends on store result. The store works as expected, when I click on certain div it's value changed (return true or false) and in my component I need to get updated value, but as I call it in ngOnInit it works only once and not updating. I've read about BehaviorSubject but don't understand how to convert my observable to it correctly. Would appreciate any help!
Here's my code
html
<div class="details__header" [attr.isOpened]="isOpened ? 'opened' : 'closed'">
ts
isOpened: boolean;
constructor(private store: Store<AppState>) { }
ngOnInit(): void {
this.store.select(selectSearchResultState).subscribe(el => this.isOpened = el);
}
I've tried async pipe in html and it works, but I need to use this variable in my code, so this doesn't work for me
You should define isOpened as an Observable like this:
isOpened$: Observable<boolean>;
constructor(private store: Store<AppState>) { }
ngOnInit(): void {
this.isOpened$ = this.store.select(selectSearchResultState);
}
Then use it in the template with an async pipe:
<div class="details__header" [attr.isOpened]="(isOpened$ | async) ? 'opened' : 'closed'">
There's more information about observables in the documentation here.
I have a function to get rates from products, so lets say I have one product with two rates. So my product has two rates. Then, when I get those rates I must get the prices attached to my product. So for each rate I have to look for its prices.
The next code below explains this:
this.loadProductInfo = true; // bool to load data in my form
// First of all, I get rates from API
// const rates = this._http....
// Now, for each rate I must search If my product/products have a price:
this.rates.forEach((rate, index, arr) => {
this._glbGetPricesForProduct.getPrice(params).subscribe(response => {
if (!arr[index + 1]) {
this.initForm();
this.loadProductInfo = false;
}
})
});
The variable loadProductInfo it loads content in my form, so in my html I have:
<form *ngIf="!loadProductInfo"></form>
But form it still give me error: could not find control name.
But if I do this instead, it works correctlly:
setTimeout(() => {
this.initForm();
this.loadProductInfo = false;
}, 2000);
So what I want its to say my form to wait until I have all code loaded and then after it, load its contents. But instead it cant find the control because it loads before code. Any help I really appreciate it.
The main mistake I see there is that you are looping over async data which may not be there when your code execute the for each loop (your rates).
I would build an observable with your rates as a source:
...
$rates: Observable<any> = this._http.get(...);
rates.pipe(
mergeMap((rates) => {
const priceByRates: Observable<any>[] = rates.map((rate, index, arr) => this._glbGetPricesForProduct.getPrice(params));
return combineLatest(pricesByRates); // if getPrice complete right away, use forkJoin() instead
})
).subscribe(res => {
// No need to check for the last item, all rates have been checked for possible price
this.initForm();
this.loadProductInfo = false;
});
...
This implementation should wait for your api calls to resolve before printing your form.
Since you are hiding the entire form, it may be better to just move the API call into a resolver so that the page does not render until the data is ready.
Here is a minimal StackBlitz showcasing this behavior: https://stackblitz.com/edit/angular-ivy-4beuww
Component
In your component, include an ActivatedRoute parameter via DI.
#Component(/*omitted for brevity*/)
export class MyComponent {
constructor(private route: ActivatedRoute) {
// note: 'data' is whatever you label your resolver prop in your routing setup
route.data.subscribe(resolved => {
if ("data" in resolved) this.resolveData = resolved["data"];
});
}
}
Route Setup
And in your router setup you would have the following:
const routes: Routes = [
{
path: 'my-route-path',
component: MyComponent,
resolve: {
data: MyResolver
}
}
];
Resolver
Finally, your resolver would make your API call utilizing your service:
#Injectable({providedIn: 'root'})
export class MyResolver() implements Resolve<T> {
constructor(private service: MyService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | any {
return this.service.myRequest();
}
}
The final result will be that your view will not be rendered until your data is ready.
I'm using Angular 9 in my web app. I'm using a lot of services to connect to a web service. Sometimes a lot of identical requests are sent to a service. Maybe when a user click on a button repeatedly.
I want to cancel previews incomplete requests for all of my services. What's the best solution? (Maybe using RXJS)
This is one of my service functions:
constructor(private http: HttpClient) {}
getList(options?: ApiListOptions) {
return this.http.post<ApiResponse<UserGet[]>>(
environment.apiRoot + 'user/list',
options,
{ headers: this.headers() }
);
}
Thanks
Please try below with takeUntil rxjs operator you will be able to do the same.
export class YourComponent {
protected ngUnsubscribe: Subject<void> = new Subject<void>();
[...]
public httpGet(): void {
this.http.get()
.takeUntil(this.ngUnsubscribe)
.subscribe( (data) => { ... })
}
public ngOnDestroy(): void {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
For your specific case, like frequent button clicks, you can use switchMap() combine with debounceTime() to limit the frequency of action and http calls. You can use Subject for action trigger instead of component method
<button click="getList.next()">click</button>
getList=new Subject()
getList.pipe(debounceTime(2000),switchMap(()=>
this.http.post<ApiResponse<UserGet[]>>(
environment.apiRoot + 'user/list',
options,
{ headers: this.headers() }
);
)).subscribe()
I am trying to test an angular2 application. I have a login form, which uses an observable to send data to the backend:
doLogin() {
this.usersService.login(this.model)
.subscribe((data) => {
console.log("In observable: " + data.isSuccess);
if (!data.isSuccess) {
this.alerts.push({});
}
});
}
In tests I am adding a spy on the service function, which returns observable, so that component can work on it:
usersService.login.and.returnValue(Observable.of(
<LoginResponse>{
isSuccess: true
}));
When everything is ready, I dispatch an event on submit button, which triggers doLogin function in component:
submitButton.dispatchEvent(new Event("click"));
fixture.detectChanges();
It works correctly. Unfortunately, when I check if usersService.login has been called in the test:
expect(usersService.login).toHaveBeenCalled();
I get an error, because the observable didn't finish and login has not been called yet.
How should I make sure, I check my spy after observable has finished?
I don't know how you configure the service on the component but it works for me when I override providers of the component created from TestComponentBuilder.
Let's take a sample. I have a service that returns a list of string:
import {Observable} from 'rxjs/Rx';
export class MyService {
getDogs() {
return Observable.of([ 's1', 's2', ... ]);
}
}
A component uses this service to display a list asynchronously when clicking a button:
#Component({
selector: 'my-list',
providers: [MyService],
template: `
<ul><li *ngFor="#item of items">{{ item }}</li></ul>
<div id="test" (click)="test()">Test</div>
`
})
export class MyList implements OnInit {
items:Array<string>;
service:MyService;
constructor(private service:MyService) {
}
test() {
this.service.getDogs().subscribe(
(dogs) => {
this.items = dogs;
});
}
}
I want to test that when I click on the "Test" button, the test method of the component is called and the getDogs method of the service is indirectly called.
For this, I create a test that instantiate directly the service and load the component using TestComponentBuilder. In this case, I need to call the overrideProviders method on it before calling createAsync. This way, you will be able to provide your spied service to be notified of the call. Here is a sample:
let service:MyService = new MyService();
beforeEach(() => {
spyOn(service, 'getDogs').and.returnValue(Observable.of(
['dog1', 'dog2', 'dog3']));
});
it('should test get dogs', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.overrideProviders(MyList, [provide(MyService, { useValue: service })])
.createAsync(MyList).then((componentFixture: ComponentFixture) => {
const element = componentFixture.nativeElement;
componentFixture.detectChanges();
var clickButton = document.getElementById('test');
clickButton.dispatchEvent(new Event("click"));
expect(service.getDogs).toHaveBeenCalled();
});
}));
Edit
Since the event is triggered asynchronously, you could consider to use fakeAsync. The latter allows you to completly control when asynchronous processing are handled and turn asynchronous things in to synchronous ones.
You could wrap your test processing into
fakeAsync((): void => {
var clickButton = document.getElementById('test');
clickButton.dispatchEvent(new Event("click"));
expect(service.getDogs).toHaveBeenCalled();
});
For more details, you could have a look at this question:
Does fakeAsync guarantee promise completion after tick/flushMicroservice
I tried passing parameters in push method of ionic2. like this
this.nav.push(SecondPage, {
thing1: data1,
thing2: data2
});
but is there any way to pass parameter in pop().
This is how I achieved it in ionic-3 and find it easier.
Page from where we pop()
this.navCtrl.getPrevious().data.thing1 =data1;
this.navCtrl.getPrevious().data.thing2 =data2;
this.navCtrl.pop();
Page after pop():
public ionViewWillEnter() {
this.thing1 = this.navParams.get('thing1')|| null;
this.thing2 = this.navParams.get('thing2')|| null;
}
I suggest you use Events. All you have to do is to subscribe to an event on the parent page and then publish the event on the child passing the data you want:
// taken from the docs
import { Events } from 'ionic-angular';
constructor(public events: Events) {}
// first page (publish an event when a user is created)
function createUser(user) {
console.log('User created!')
events.publish('user:created', user);
}
// second page (listen for the user created event)
events.subscribe('user:created', (userEventData) => {
// userEventData is an array of parameters, so grab our first and only arg
console.log('Welcome', userEventData[0]);
});
Currently, I believe that there is no way of accomplishing this.
There is a Github issue for it though, that has got some great discussion on it by the Ionic core team. It sounds like they have added it to the Ionic 2 roadmap, too! The Github issue also has some proposed work-arounds, such as adding the ParentPage to the NavParams going to the ChildPage, but it is all quite a bit hacky.
UPDATE: IT WAS SUPPOSED TO WORK, BUT IT DOES NOT
Seems like there is |See Doc Reference|
pop(opts) takes one parameter of type object
so
to go one step back
this.nav.pop({
thing1: data1,
thing2: data2
});
and to go to a specific view in the history stack
this.nav.popTo(SecondPage, {
thing1: data1,
thing2: data2
});
and to go to root of the stack
this.nav.popToRoot({
thing1: data1,
thing2: data2
});
To retrieve the params (I guess this should work. untested!)
export class SecondPage{
constructor(params: NavParams){
this.params = params;
console.log(this.params.get('thing1'));
}
}
Use popTo() instead of pop()
popTo() has params? parameter where you can pass in your paramaters like so:
this.nav.popTo(SecondPage, {
thing1: data1,
thing2: data2
});
pass in a callback when transitioning by aaronksaunders in this forum
https://forum.ionicframework.com/t/solved-ionic2-navcontroller-pop-with-params/58104/4
Going to try it out.
For sent data with pop you can use getPrevious() method.
When leaving from current page get previous page and send data
ionViewWillLeave() {
this.navCtrl.getPrevious().data.formData = {credentials: {...}};
}
In next page get data from navParams
ionViewWillEnter() {
if (this.navParams.get('formData')) {
// do something
}
}
If you are using ionic-angular application, you can use ionic-angular Events
page1.ts
import { Events,NavController } from 'ionic-angular';
export class page1 {
constructor(private events: Events,
private nvCtrl: NavController
) {}
goToPage2() {
this.navCtrl.pop();
this.event.publish('your-event');
}
}
page2.ts
import { Events} from 'ionic-angular';
export class page1 {
constructor(private events: Events,
private nvCtrl: NavController
) {}
ionViewDidLoad() {
this.events.subscribe('your-event');
}
}