Disclaimer: I'm super new to both Angular and RXJS.
I have a simple form from which I'm trying to create an observable. This will look for submit events and update some value in the component. However, I'm getting a this._subscribe is not a function error.
<form (submit)='submitForm()'>
<button type='submit'>Submit</button>
</form>
My component
import { Component, OnInit } from '#angular/core';
import { Observable } from 'rxjs';
import UtilsHelperService from '../services/utils-helper.service';
#Component({...stuffs...})
export class HomeComponent implements OnInit {
formSubmit: Observable<any>;
counter = 0;
constructor() { }
ngOnInit() {
const form = document.getElementsByTagName('form')[0];
this.formSubmit = Observable.create(form, 'submit');
}
submitForm() {
this.formSubmit.subscribe(
UtilsHelperService.formSubmitObserver(this.counter));
}
}
And my utils-helper.service.ts helper class...
import {Injectable} from '#angular/core';
#Injectable({
providedIn: 'root'
})
export default class UtilsHelperService {
static formSubmitObserver(counter) {
return {
next: (value) => {
counter++;
},
error: err => console.log(err),
complete: () => console.log('complete')
}
}
}
I see that the formSubmit observer is created fine.
I have the UtilsHelperService.formSubmitObserver method that returns an observer object with the 3 necessary methods.
So, I'm not sure whether if it's the Angular stuffs I'm doing wrong (which I guess not) or its the RXjs stuff. Thank you for your time reading it :)
Take a look at FormGroup. Its 'valueChanges' property is an observable you can subscribe to.
FormGroup formGroup;
// populate your formGroup (https://angular.io/guide/reactive-forms#step-1-creating-a-formgroup-instance)
formGroup.valueChanges.subscribe(// do whatever you want);
There were 2 things I did to solve the issue:
Using fromEvent instead of Observable.create to create observable from submit event. This way the subscription didn't threw error (investigating why..)
Updating component property from service won't work as the services are singletons. You either have to use eventemitter or use AngularJS styles dot rule. For this case, I added all the helper logic in the component itself.
ngOnInit() {
const form = document.getElementsByTagName('form')[0];
this.formSubmit = fromEvent(form, 'submit');
this.formSubmit.subscribe((submitEvent) => {
this.counter++;
this.formSubmitted.emit(this.counter);
})
}
With this I can remove the submitForm method from the component and template and the helper method from the service.
Related
In an Angular 11 app, I have a simle service that mekes a get request and reads a JSON.
The service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Promo } from '../models/promo';
#Injectable({
providedIn: 'root'
})
export class PromoService {
public apiURL: string;
constructor(private http: HttpClient) {
this.apiURL = `https://api.url.com/`;
}
public getPromoData(){
return this.http.get<Promo>(`${this.apiURL}/promo`);
}
}
In the the component, I need to compare the array of products with the array of campaign products (included in the JSON mantioned above) and higlight the promoted products:
export class ProductCardComponent extends DestroyableComponent implements OnInit, OnChanges
{
public promoData: any;
public promoProducts: any;
public isPromoProduct: boolean = false;
public ngOnInit() {
this.getCampaignData();
}
public ngOnChanges(changes: SimpleChanges): void {
this.getCampaignData();
}
public getPromoData() {
this.promoService.getPromoData().pipe(takeUntil(this.destroyed$)).subscribe(data => {
this.promoData = data;
this.promoProducts = this.promoData.products;
let promoProduct = this.promoProducts.find((product:any) => {
return this.product.unique_identifier == product.unique_identifier;
});
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
}
});
}
}
In the component's html file (template), I have:
<span *ngIf="isPromoProduct" class="promo">Promo</span>
There are no compilation errors.
The problem
For a reason I have been unable to understand, the template does not react to the change of the variable isPromoProduct and the template is not updated, despite the fact that I call the function inside ngOnInit and ngOnChanges.
Questions:
Where is my mistake?
What is a reliable way to update the template?
subscribing to Observable inside .ts file it's mostly not a best practice.
try to avoid it by using async pipe of Angular.
you need to store the observable in the variable and not the data returned from the observable, for example:
// this variable holds the `observable` itself.
this.promoData$ = this.promoService.getPromoData()
and then in the template you can do it like this:
<div *ngIf="promoData$ | async as promoData">
here you can access the promoData
</div>
you can still use pipe() to map the data etc but avoid the subscribe()
The isPromoProduct boolean is not an input. The ngOnChanges gets triggered for changes on your properties that are decorated with the #Input decorator. For your particular case, you can inject the ChangeDetectorRef and trigger change detection manually:
constructor(private cdr: ChangeDetectorRef) {}
// ...
public getPromoData() {
this.promoService.getPromoData().subscribe(data => {
// ...
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
this.cdr.detectChanges();
}
});
}
You also don't need to manage httpClient subscriptions. The observables generated by a simple get or post request will complete after they emit the response of the request. You only need to explicitly manage the unsubscribe for hot observables (that you create from subjects that you instantiate yourself).
Lets say I have 2 components, aComponent and bComponent. I have them redered inside the AppComponent
<app-a>
<app-b>
And I have service myService that has method .trigger().
What I want is to show only aComponent, but whenever I call myService.trigger() from another part of code, it would switch and show bComponent. That's perfect implementation that I can't reach.
Question is: Is it possible to do so? And if not what is the best closest solution.
The only working solution I got:
I added .trigger() inside AppComponent
export class AppComponent {
title = 'spa';
show: boolean = false;
trigger() {
this.show = true;
}
}
And rendered components like so:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
Then whenever I want to trigger switching, I add instance of the app to the constructor and call it's method:
export class AnotherComponent implements OnInit {
constructor(
private app: AppComponent
) {}
ngOnInit(): void {
this.app.trigger();
}
}
Even though it's working pretty good, I myself see that it's a dirty solution. Components are not intended to be used inside another components, but Services are.
You can use Subject from rxjs library for that.
In your service file:
// a-service.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable({ providedIn: 'root' })
export class AService {
private subject = new Subject<any>();
trigger(state: boolean) {
this.subject.next(state);
}
getTrigger(): Subject<any> {
return this.subject;
}
}
and in your AppComponent:
// app.component.ts
...
private show = false;
constructor (private aService: AService) { }
ngOnInit() {
this.aService.getTrigger().subscribe(state => {
this.show = state;
});
}
the template can be as you provided - it's fine:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
And if you want to trigger from another component, you do it like this:
// another.component.ts
...
constructor (private aService: AService) { }
ngOnInit() {
this.aService.trigger(true);
}
One way to communicate between different components and services which aren't directly related, is via 'Subjects'.
You can try to create a subject and pass in values to it from myService.trigger(). And you can subscribe to that subject from whichever component you want to access that trigger data.
I am using 'angular2-virtual-scroll' to implement load on demand. The items used to be driven by observable's using the async pipe triggered by the parent component. Now i am trying to call my service from the child. The call is successful and i get my data, i need to use the subscribe event to apply other logic. The issue is change detected does not appear to be working when i update my arrays in the subscribe function. I have read other similar issues but i have had no luck finding a solution.
This is the main component where the service calls are used. The inital request is done from the onInit. And then when you scroll down fetchMore is called.
import { Component, OnInit, Input, OnDestroy } from '#angular/core';
import { Store } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { User } from './../models/user';
import { Role } from './../../roles/models/role';
import { UsersService } from './../services/users.service';
import { ChangeEvent } from 'angular2-virtual-scroll';
import { promise } from 'selenium-webdriver';
import { VirtualScrollComponent } from 'angular2-virtual-scroll';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'app-users-list',
template: `
<div class="status">
Showing <span class="">{{indices?.start + 1}}</span>
- <span class="">{{indices?.end}}</span>
of <span class="">{{users?.length}}</span>
<span>({{scrollItems?.length}} nodes)</span>
</div>
<virtual-scroll [childHeight]="75" [items]="users" (update)="scrollItems = $event" (end)="fetchMore($event)">
<div #container>
<app-user-info *ngFor="let user of scrollItems" [roles]="roles" [user]="user">
<li>
<a [routerLink]="['/users/edit/', user.id]" class="btn btn-action btn-edit">Edit</a>
</li>
</app-user-info>
<div *ngIf="loading" class="loader">Loading...</div>
</div>
</virtual-scroll>
`
})
export class UsersListComponent implements OnInit, OnDestroy {
users: User[] = [];
#Input() roles: Role[];
currentPage: number;
scrollItems: User[];
indices: ChangeEvent;
readonly bufferSize: number = 20;
loading: boolean;
userServiceSub: Subscription;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.reset();
}
ngOnDestroy() {
if(this.userServiceSub) {
this.userServiceSub.unsubscribe();
}
}
reset() {
this.loading=true;
this.currentPage = 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = users;
});
}
fetchMore(event: ChangeEvent) {
if (event.end !== this.users.length) return;
this.loading=true;
this.currentPage += 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = this.users.concat(users);
});
}
}
From what i have read this could be a context issue but i am not sure. Any suggestions would be great.
"EDIT"
Looking at the source code for the plugin component i can see where the change event is captured.
VirtualScrollComponent.prototype.ngOnChanges = function (changes) {
this.previousStart = undefined;
this.previousEnd = undefined;
var items = changes.items || {};
if (changes.items != undefined && items.previousValue == undefined || (items.previousValue != undefined && items.previousValue.length === 0)) {
this.startupLoop = true;
}
this.refresh();
};
If i put a breakpoint in this event it fires on the initial load, so when we instantiate the array to []. It fires when i click on the page. But it does not fire when the array is update in the subscribe event. I have even put a button in that sets the array to empty, and that updates the view so the subscribe function must be breaking the change detection.
So when you say the change detection does not appear to be working, I assume you are referring to this: *ngFor="let user of scrollItems"?
I have not used that particular component nor do I have any running code to work with ... but I'd start by taking a closer look at this:
<virtual-scroll [childHeight]="75"
[items]="currentBuffer"
(update)="scrollItems = $event"
(end)="fetchMore($event)">
Maybe change the (update) to call a method just to ensure it is emitting and that you are getting what you expect back from it.
EDIT:
Here is an example subscription that updates the primary bound property showing the data for my page:
movies: IMovie[];
getMovies(): void {
this.movieService.getMovies().subscribe(
(movies: IMovie[]) => {
this.movies = movies;
this.performFilter(null);
},
(error: any) => this.errorMessage = <any>error
);
}
The change detection works fine in this case. So there is most likely something else going on causing the issue you are seeing.
Note that your template does need to bind to the property for the change detection to work. In my example, I'm binding to the movies property. In your example, you'd need to bind to the users property.
So change detection was not firing. I had to use "ChangeDetectorRef" with the function "markForCheck" to get change detection to work correctly. I am not sure why so i definitely have some research to do.
I have a ShareService in angular 2,
******************************shareService*************************
import { BehaviorSubject , Subject} from 'rxjs/Rx';
import { Injectable } from '#angular/core';
#Injectable()
export class shareService {
isLogin$:BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
CheckUser = this.isLogin$.asObservable();
public isLogin (bool){
this.isLogin$.next(bool);
}
}
and its my another component and subscibe the CheckUser;
***********************another Component*******************************
_shareService.CheckUser.subscribe((val) =>{
*********all of this scope execute for several times just i have one another component and one next function*******
this.isLogin = val;
alert(val);
if(this.isLogin){
console.log("req req req");
this.MyBasket();
}
else if(this.ext.CheckLocalStorage("ShopItems")){
this.ShopItems = JSON.parse(localStorage.getItem("ShopItems"));
setTimeout(() => {
_shareService.sendShopItems(this.ShopItems);
},100);
}
});
my problem is i execute once this.isLogin$.next(bool) but subscribe function execute twice or several times !!!! my basket function is an xhr request this means when user loged in i get the several request to server!!!i cant fix it...i dont know this problem is for angular 2 or not,Anyone have this problem??
last a few days i Involved in this problem!
The problem is that your shareService is getting multiple instances.
One of the solutions is forcing the service to be a singleton.
Something like this should work:
import { provide, Injectable } from '#angular/core';
#Injectable()
export class shareService {
private static instance: shareService = null;
// Return the instance of the service
public static getInstance(/*Constructor args*/): shareService {
if (shareService.instance === null) {
shareService.instance = new shareService(/*Constructor args*/);
}
return shareService.instance;
}
constructor(/*Constructor args*/) {}
}
export const SHARE_SERVICE_PROVIDER = [
provide(shareService, {
deps: [/*Constructor args dependencies*/],
useFactory: (/*Constructor args*/): shareService => {
return shareService.getInstance(/*Constructor args*/);
}
})
];
Everything that is required on your current constructor should be placed where it says constructor args
Now on your components you use the service like this:
#Component({
providers: [SHARE_SERVICE_PROVIDER]
})
And then you can call it like you usually do.
Another solution would be injecting your current service on the main component of the app. See here for more info.
The problem is that the service is singleton and the component subscribe to it each time it created or (I don't see the full code) at the point the
_shareService.CheckUser.subscribe
is placed , so CheckUser should be a method that returns an Observable . if you have plunkr I can edit it .
Another semantic problem is that the observable should end with $ and not the BehaviorSubject.
I know how to raise an event with the EventEmitter. I can also attach a method to be called if I have a component like this:
<component-with-event (myevent)="mymethod($event)" />
When I have a component like this, everything works great. I moved some logic into a service and I need to raise an event from inside the Service. What I did was this:
export class MyService {
myevent: EventEmitter = new EventEmitter();
someMethodThatWillRaiseEvent() {
this.myevent.next({data: 'fun'});
}
}
I have a component that needs to update some value based on this event but i can't seem to make it work. What I tried was this:
//Annotations...
export class MyComponent {
constructor(myService: MyService) {
//myService is injected properly and i already use methods/shared data on this.
myService.myevent.on(... // 'on' is not a method <-- not working
myService.myevent.subscribe(.. // subscribe is not a method <-- not working
}
}
How do i make MyComponent subscribe to the event when the service that raises it is not a component?
I'm on On 2.0.0-alpha.28
EDIT: Modified my "working example" to actually work, so focus can be put on the not-working part ;)
Example code:
http://plnkr.co/edit/m1x62WoCHpKtx0uLNsIv
Update: I have found a better/proper way to solve this problem using a BehaviorSubject or an Observable rather than an EventEmitter. Please see this answer: https://stackoverflow.com/a/35568924/215945
Also, the Angular docs now have a cookbook example that uses a Subject.
Original/outdated/wrong answer: again, don't use an EventEmitter in a service. That is an anti-pattern.
Using beta.1... NavService contains the EventEmiter. Component Navigation emits events via the service, and component ObservingComponent subscribes to the events.
nav.service.ts
import {EventEmitter} from 'angular2/core';
export class NavService {
navchange: EventEmitter<number> = new EventEmitter();
constructor() {}
emitNavChangeEvent(number) {
this.navchange.emit(number);
}
getNavChangeEmitter() {
return this.navchange;
}
}
components.ts
import {Component} from 'angular2/core';
import {NavService} from '../services/NavService';
#Component({
selector: 'obs-comp',
template: `obs component, item: {{item}}`
})
export class ObservingComponent {
item: number = 0;
subscription: any;
constructor(private navService:NavService) {}
ngOnInit() {
this.subscription = this.navService.getNavChangeEmitter()
.subscribe(item => this.selectedNavItem(item));
}
selectedNavItem(item: number) {
this.item = item;
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
#Component({
selector: 'my-nav',
template:`
<div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>
<div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>
`,
})
export class Navigation {
item = 1;
constructor(private navService:NavService) {}
selectedNavItem(item: number) {
console.log('selected nav item ' + item);
this.navService.emitNavChangeEvent(item);
}
}
Plunker
Using alpha 28, I accomplished programmatically subscribing to event emitters by way of the eventEmitter.toRx().subscribe(..) method. As it is not intuitive, it may perhaps change in a future release.
Sometime quick fix of library cause that added event import like
import { EventEmitter } from 'events';
You must change it with core libray using subscribe
import { EventEmitter } from '#angular/core';