I have a a function that returns an Observable<Person[]>, Person is my model:
export interface Person {
id: string;
age: number;
}
Now in my component.ts im calling this function and I want to retrieve that array so I can display it in the html.
the array that comes back example:
[{"id":"13434 1","age":21},{"id":"34334 1","age":27}]
I have 2 buttons that call the same function.
I have three methods in my component.ts that triggered when that buttons where clicked, and this is where I want to set some variable to hold the returned value of that Observable. I'v tried to do something but it didnt work...
this are the functions:
#Injectable()
export class MyCmp implements OnInit {
listOneData: Observable<Person[]>;
listTwoData: Observable<Animal[]>;
showListOne = false;
showListTwo = false;
constructor(private _myService: MyService) {
};
public showListOneData(): void {
this.showListOne = true;
this.showListTwo = false;
this._myService.getListOneData().subscribe(res => {
this.listOneData = res;
})
}
public showListTwo(): void {
this.showListTwo = true;
this.showListOne = false;
this._myService.getListTwoData().subscribe(res => {
this.listTwoData = res;
})
}
}
this.listTwoData = res; this line does not compile, its because im assigning Person[] to listOneData: Observable<Person[]>;, but even if I take of the Observable<> it dosent work.
Can someone please explain to me what woill be an efficiant way to do it? I dont want to do it async cause I want to send an array to the html, and base on the code where should I do unsubscribe?
thanks allot!
Sounds like you want two different things; data from the observable and a subscription object so you can unsubscribe:
#Injectable()
export class MyCmp implements OnInit {
listOneData: Person[];
listTwoData: Animal[];
listOneSubscription: Subscription;
listTwoSubscription: Subscription;
showListOne = false;
showListTwo = false;
constructor(private _myService: MyService) {
};
public showListOneData(): void {
this.listOneSubscription = this._myService.getListOneData().subscribe(res => {
this.showListOne = true;
this.showListTwo = false;
this.listOneData = res;
})
}
public showListTwo(): void {
this.listTwoSubscription = this._myService.getListTwoData().subscribe(res => {
this.showListTwo = true;
this.showListOne = false;
this.listTwoData = res;
})
}
}
Then, wherever you're wanting to unsubscribe:
this.listOneSubscription && this.listOneSubscription.unsubscribe();
this.listTwoSubscription && this.listTwoSubscription.unsubscribe();
You also mentioned you don't want to do it async; that doesn't make sense in JavaScript since this is asynchronous; ask yourself, what do you want to show to the user while the content is loading? You probably want to show some sort of progress indicator, so that should be built into your view.
Related
I am dealing with an error which when I try to create new page Object, it send to backend but it is not updating the array, I need to reload the page to see the all the array.
I am using Observable within async in the frontend.
I tried to console.log the ngOnInit of the page.component.ts but when I add new page and navigate to pages then the ngOnInit it isn't calling.
On Create new page it happens this.
It sends me to the route of pages where there I show all the list of pages.
But when I create new Page it is returningback an error which says.
ERROR Error: Error trying to diff 'Here is the name of the object'. Only arrays and iterables are allowed.
Update: as Marco said this happens because I mix page as Object instead I am iterating through array
But I am unable to resolve it and i need your help.
In the page.service.ts at pageModel when I add new Object it is returning me only the added Object not the whole array and there is the problem I think, but I don't know how to fix.
But If I reload page then I see all my Array.
This is my updated code.
This is my code.
export class PagesService {
public baseUrl = environment.backend;
private data = new ReplaySubject<any>();
public userID = this.authService.userID;
public editDataDetails: any = [];
public subject = new Subject<any>();
private messageSource = new BehaviorSubject(this.editDataDetails);
getPageID = this.messageSource.asObservable();
constructor(private http: HttpClient, private authService: AuthService) { }
public getPages() {
return this.http.get<any>(`${this.baseUrl}/pages/${this.userID}`).subscribe(res => this.data.next(res));
}
public pageModel(): Observable<Page[]> {
return this.data.asObservable(); // Here it throws error
}
public getPage(id): Observable<any> {
return this.http.get(`${this.baseUrl}/page/${id}`);
}
public setPage(page: Page, id: string) {
const api = `${this.baseUrl}/page`;
const user_id = id;
this.http.post<any>(api, page, {
headers: { user_id }
}).subscribe(res => this.data.next(res));
}
changeMessage(message: string) {
this.messageSource.next(message)
}
public updateDate(id: string, page: Page) {
const api = `${this.baseUrl}/page/${id}`;
return this.http.put<any>(api, page).subscribe(res => this.data.next(res.data));
}
Updated Code from Answer.
public updateDate(id: string, page: Page) {
const api = `${this.baseUrl}/page/${id}`;
return this.http.put<any>(api, page).subscribe(res => {
this.lastSetOfData = res;
this.data.next(this.lastSetOfData);
});
}
}
export class Page {
_id = "";
name = "";
slogan = "";
description = "";
url = "";
telephone: number;
pageUrl: string;
website: string;
founded: number;
organization: number;
email: string;
coverImage: string;
profileImage: string;
specialty?: Specialty[];
branches: Branches[];
locations?: Location[];
phone?:Phone;
userRole?: string;
roles?: Roles[];
}
export class Roles {
role= "";
userID = "";
}
This is the HTML of page.component .
<div class="main" *ngIf="!showWeb">
<div *ngFor="let page of pages$ | async" class="card width-900">
<app-pages-list class="d-flex width-900" [page]="page" [details]="'details'"></app-pages-list>
</div>
<div>
</div>
</div>
This is the TS file.
public pages$: Observable<Page[]>;
ngOnInit(): void {
this.pageService.getPages();
this.pages$ = this.pageService.pageModel();
}
And this is the code when I create new Page.
export class CreatePageComponent implements OnInit {
public page = new Page();
search;
public branch = [];
constructor(public router: Router,
public branchesService: BranchesService,
public authService: AuthService,
public pageService: PagesService,
public shareData: SenderService) { }
ngOnInit(): void {
}
createPage() {
this.page.url = this.page.name;
this.page.branches = this.branch;
this.page.locations = [];
this.page.specialty = [];
this.page.roles = [];
this.page.phone = this.page.phone;
this.page.pageUrl = `${this.page.name.replace(/\s/g, "")}${"-Page"}${Math.floor(Math.random() * 1000000000)}`;
this.pageService.setPage(this.page, this.authService.userID);
}
addBranch(event) {
this.branch.push(event);
this.search = "";
}
removeBranch(index) {
this.branch.splice(index, 1);
}
}
From my understanding of your code, your error is thrown because the data variable hold 2 types of objects.
In the PagesServices:
In getPages you give data a list of Page.
In setPage and updatePage you give data an instance of Page.
private data = new ReplaySubject<any>();
When you create a new page, data hold the last page you created (not an array). Then you try to iterate this page.
<div *ngFor="let page of pages$ | async"
This error come from the fact that you can't iterate a Page object.
You should stop using any so that this type of error occurs at compilation time, not at runtime. Also you need to store an instance of the array of page, add the item in your array after a post, and then replay the whole array.
Code
public updateDate(id: string, page: Page) {
const api = `${this.baseUrl}/page/${id}`;
return this.http.put<any>(api, page).subscribe((res) => {
const index: number = lastSetOfData.findIndex((_page: Page) => _page._id === res._id);
lastSetOfData[index] = res;
lastSetOfData = [...lastSetOfData];
this.data.next(lastSetOfData);
});
}
Also the updateDate function should be named updatePage.
The issue is the one identified in the response from #Marco. I elaborate starting from there.
There are several ways of fixing this problem. Probably the fastest is to add an instance variable lastSetOfData to PagesService where you hold the last version of the array. Then you initiatlize lastSetOfData in the getPages method. Finally in the setPage method you update lastSetOfData appending the Page returned by the service at the end of lastSetOfData and notify it using the ReplaySubject.
So the code could look like this
export class PagesService {
public baseUrl = environment.backend;
// specify the type of data notified by the ReplaySubject
private data = new ReplaySubject<Array<Page>>();
// define lastSetOfData as an array of Pages
private lastSetOfData: Array<Page> = [];
....
public getPages() {
return this.http.get<any>(`${this.baseUrl}/page/${this.userID}`).subscribe(res => {
// res should be an array of Pages which we use to initialize lastSetOfData
lastSetOfData = res;
this.data.next(lastSetOfData)
});
}
....
public setPage(page: Page, id: string) {
const api = `${this.baseUrl}/page`;
const user_id = id;
this.http.post<any>(api, page, {
headers: { user_id }
}).subscribe(res => {
// update lastSetOfData appending resp, which should be a Page
// not the use of the spread operator ... to create a new Array
lastSetOfData = [...lastSetOfData, resp];
// now you notify lastSetOfData
this.data.next(lastSetOfData)
});
}
// probably you have to modify in a similar way also the method updateTable
public updateDate(id: string, page: Page) {
....
}
....
....
}
Consider that this may be the fastest way to fix the problem. Check if it works and then you may want to try to refactor the code to look for a more rx-idiomatic solution. But my suggestion is first to see if this fixes the problem.
Problem is that you put an object in your replaysubject although an array is expected in other places.
next(myarray)
next(myobject)
This does not magically append an object to the array.
To do so, you'd need something like this:
data.pipe(take(1)).subscribe(list => {
list.push(newvalue);
data.next(list);
});
Basically you take the last value, a the new item, and push the new list.
I am working with Ionic and I want to push an array of an object, when an event is emitted.
I have this
export class PublicationService {
constructor(
private storage: Storage
){}
private addPublicationSubject = new BehaviorSubject<PublicationModel>(new PublicationModel());
data = this.addPublicationSubject.asObservable();
publishData(data: PublicationModel) {
this.addPublicationSubject.next(data);
}
}
Here, event is emitted
savePublication() {
this.newPublication.id_pub = 1;
this.newPublication.textPub = this.f.text.value;
this.newPublication.user_sender = 'Juance';
this.newPublication.datetime = "asd";
this.pubService.publishData(this.newPublication);
}
And on my home page the event is listen (in ngOnInit)
// Variable defined in the component
publications: PublicationModel[] = [];
//ngOnInit
this.pubService.data.subscribe((data) => {
if (data != null) {
console.log(data);
this.publications.push(data);
}
});
Now my problem is: when I try to push the data into the array it tells me it cannot read property of null (this.publications).
When entering the subscribe of the event, it does not take the variable as defined in the component. Any ideas?
EDIT:
My component HomePage
#Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {
viewAddPublication: boolean;
publications: PublicationModel[] = [];
countLikePub: number;
addPublication: any;
constructor(
private storage: Storage,
private navCtrl: NavController,
private pubService: PublicationService) {
this.publications = new Array<PublicationModel>();
}
ngOnInit() {
this.publications = [];
this.pubService.data.subscribe((data) => {
if (data != null) {
console.log(data);
this.setData(data);
}
}
);
this.viewAddPublication = false;
this.countLikePub = 0;
this.storage.get('publications').then((val) => {
this.publications = val;
});
}
setData(data) {
this.publications.push(data);
}
goToAddPub() {
this.navCtrl.navigateForward('/add-publication', {
animated: true,
animationDirection: "forward",
});
}
public likedPost(event) {
console.log();
let like = (document.getElementById(event.target.id) as HTMLElement);
like.style.color = '#0277bd';
this.countLikePub++;
}
debug mode in chrome
I need a way to push an array in real time and this is the only way I could think of, the other is to use Socket.io
I think maybe you are setting publications to null because of this function:
this.storage.get('publications').then((val) => {
this.publications = val;
});
You could change it a little bit to make sure publications are still an array
this.storage.get('publications').then((val) => {
this.publications = val || [];
});
I added this.publications = val || []; which is creating an empty array if val is not defined
I am writing an Angular Service to prove a Users permissions. In the constructor I want to get the current logged in user from an API. The current User which is created is used in other methods of this Service. This Methods are called from components to check which things can be shown and so on.
The problem is that the methods in the service are called faster than the current user is available.
Are there any possibilities solving this issue?
permission.service.ts
#Injectable()
export class PermissionService {
currUser = {
'id': "",
'permission': ""
};
apiService: AlfrescoApiService;
authService: AuthenticationService;
constructor(apiService: AlfrescoApiService, authService: AuthenticationService) {
this.apiService = apiService;
this.authService = authService;
this.init();
}
init() {
let userId: string = this.authService.getEcmUsername();
this.currUser.id = userId;
//API call
this.apiService.sitesApi.getSiteMember(SITENAME, userId).then(resp => {
this.currUser.permission = resp.entry.role;
})
}
isSiteManager(): boolean {
console.log(this.currUser.permission, this.currUser);
if(this.currUser.permission === "SiteManager"){
return true;
}else{
return false;
}
}
}
method call
export class AppLayoutComponent {
constructor(permissionService:PermissionService) {
permissionService.isSiteManager();
}
}
output in Google Chrome
{id: "admin", permission: ""}
id: "admin"permission: "SiteManager"
You should use promise in your getEcmUsername() handle this; after that you can code like this
`
this.authService.getEcmUsername().then((userID) => {
this.apiService.sitesApi.getSiteMember(SITENAME, userId).then(resp => {
this.currUser.permission = resp.entry.role;
})
});
`
In my opinion better solution is to use Observable here and rxjs. In service you can create Subject and subscribe it inside your compnent, to be sure data is already there. E.g.:
#Injectable()
export class PermissionService {
public Subject<bool> userFetched= new Subject<bool>();
currUser: IUser = {
'id': "",
'permission': ""
};
apiService: AlfrescoApiService;
authService: AuthenticationService;
constructor(apiService: AlfrescoApiService, authService: AuthenticationService) {
this.apiService = apiService;
this.authService = authService;
this.init();
}
init() {
let userId: string = this.authService.getEcmUsername();
this.currUser.id = userId;
//API call
this.apiService.sitesApi.getSiteMember(SITENAME, userId).subscribe((data:IUser)=>
{
this.user=data;
this.userFetched.next(true);
})
}
isSiteManager(): boolean {
console.log(this.currUser.permission, this.currUser);
if(this.currUser.permission === "SiteManager"){
return true;
}else{
return false;
}
}
}
After that in your component:
export class AppLayoutComponent {
constructor(permissionService:PermissionService) {
permissionService.userFetched.subscribe((data)=>{
permissionService.isSiteManager();
});
}
}
It's better approach. You need to consider if better is Subject or BehaviourSubject.
Thanks to everybody who answered. I have found a solution. I have changed the isSiteManager() method to a method which checks all four permissiontypes. This method is executed in the then() block and affects four variables for each permissiontype. These variables i can reach from other components.
Looks like this:
#Injectable()
export class PermissionService {
isSiteManager: boolean;
isSiteConsumer: boolean;
isSiteContributor: boolean;
isSiteCollaborator: boolean;
userId : string;
constructor(private apiService: AlfrescoApiService, private authService: AuthenticationService) {
this.init();
}
init() {
this.isSiteCollaborator = false;
this.isSiteConsumer = false;
this.isSiteContributor = false;
this.isSiteManager = false;
this.userId = localStorage.USER_PROFILE;
//proof permission of user
this.apiService.sitesApi.getSiteMember(SITENAME, this.userId).then(resp=>{
if(resp.entry.role === "SiteManager"){
this.isSiteManager = true;
}else if(resp.entry.role === "SiteConsumer"){
this.isSiteConsumer = true;
}else if(resp.entry.role === "SiteContributor"){
this.isSiteContributor = true;
}else{
this.isSiteCollaborator = true;
}
});
}
}
Now i can ask for the variables in other components like this:
export class AppLayoutComponent {
constructor(private permissionService : PermissionService) {
if(permissionService.isSiteManager){
console.log("You are Boss!");
}
}
}
You should call your service method synchrony. To do so, you have to map your response from the service:
your component code:
constructor() {
...
permissionService.isSiteManager().map(
response => {
isManager = response;
}
);
}
Something like this.
To call map operator import it before:
import 'rxjs/add/operator/map';
I want to pass an object between 2 components. I created the following shared service:
[PageService Component]
private messageSource = new BehaviorSubject([]);
currentMessage = this.messageSource.asObservable();
changeMessage(message) {
this.messageSource.next(message)
}
And I have implemented it in these 2 components:
[COMPONENT WHEN I GET ON CLICK SONO DATAS]
constructor(private pageService: PageService, private _sanitizer: DomSanitizer) {}
...
onClickMethod(){
self.pageService.getCustomers(self.filters).toPromise().then(response => {
self.searchResults = response;
});
self.pageService.changeMessage(self.searchResults);
}
and
[Component where I need to see above datas]
ngOnInit() {
let self = this;
self.pageService.currentMessage.subscribe(message => self.searchResults = message);
console.log(self.searchResults);
}
Now...if I put the "changeMessage" method in the first component in the method onInit or in the costructor and i try to pass some data like [1,2,3] (so not the response of another api rest) it seems to work...this doesn't work just when i put it inside onClick method and passing "self.searchResults" (the response)...anyone can help me?
Thanks
Go from this
self.pageService.getCustomers(self.filters).toPromise().then(response => {
self.searchResults = response;
});
self.pageService.changeMessage(self.searchResults);
To this
self.pageService.getCustomers(self.filters).toPromise().then(response => {
self.searchResults = response;
self.pageService.changeMessage(self.searchResults);
});
Because you make an HTTP call (I assume), you should wait for the call to end. In your code, it doesn't.
I am working on an Angular2 application and one of the #Components has a button that when clicked will send a post request to my server which will either respond with an Ok(string) or a BadRequest(string).
I am having trouble updating an #Input field of one of my #Components after getting the answer from the server.
Below are simplified version of some of my classes.
My Component class
#Component({
moduleId: module.id,
selector: 'model-comp',
templateUrl: './model.component.html',
styleUrls: ['./model.component.css']
})
export class MyComponent{
#Input() model: Model;
#Output() emitter: EventEmitter<Model> = new EventEmitter<Model>();
public constructor(private service: MyService){}
public toggle(): void {
this.service.send(model.id, model.name){
.subscribe(
result => this.onSuccess(result)),
error => this.onError(error),
() => this.onComplete());
}
public onSuccess(result: string): void {
if(result.inculdes("Some Text")) this.model.flag = true;
else this.model.flag = false;
this.emitter.emit(this.model);
}
public onError(error: any): void {
//notification using bootstrap-notify
}
public onComplete(): void {
//currently empty
}
}
My Service class
export class MyService{
public send(id: string, name: string){
return <Observable<string>>this.http
.post('url', new Dto(id, name))
.map(result => this.getData<string>(result))
.catch(this.catchBadResponse);
}
private getData<E>(result: Response): E {
//checking if result.status is ok
var body = result.json ? res.json(): null;
return <E>(body || {});
}
private catchBadRespomse: (error: any) => Observable<any> = (error: any) => {
var response = <Response>error;
var json = response.json();
var msg = json.Message;
var errormsg = json?
(json.error ? json.error: JSON.stringify(msg?msg:json)) :
(response.statusText || 'Error?');
return Obserable.of(errormsg);
}
}
Template of MyComponent
<button (click)="toggle()"
[ngClass]="{'class1': true, 'class2': model.flag}">Text</button>
Template of Parent Component
<div *ngFor="let model of getList()">
<model-comp [model]="model" (emitter)="onEmit($event)"></model-comp>
</div>
The onEmit Function
onEmit(evt: any): void{
if(evt instanceof Model){
var evtModel = evt as Model;
this.list.find(search => search.id == evtModel.id)
.isFav = evtModel.isFav;
}
}
The problem is that even though I post my data and receive the response, The property flag of my model does not change.
I think that the click event reloads the component thus removing the observers of the EventEmitter.
So is there any way to cancel the reload, not lose the observers of the EventEmitter or any other way to update the root object or the element class?
update (see comments below the question)
If getList() (what *ngFor binds to) returns a new list every time it is called, *ngFor will be permanently busy rerendering the items because change detection will cause getList() being called again and again.
Binding to a function that returns a new object or array every time it's called directly will cause serious issues like exceptions and dramatic performance degredation.
Using method/function calls in the view is strongly discouraged in general. Rather assign the list to a field and bind to that field instead of the method.
ngOnInit() is fine for initializing the list but also any event handler for initializing or updating the list.
original
If you modify the model value that you got passed in from the parent, then the parent also sees the change. Emitting the value as an event is probably redundant.
I guess you are modifying list (from <div *ngFor="let model of list">) in onEmit() which then causes *ngFor to rerender the list.
I don't think you should change #input property from within the component.
it suppose to listen and act to changes from the parent component.
MyComponent.ts
export class MyComponent{
#Input() model: Model;
//#Output() emitter: EventEmitter<Model> = new EventEmitter<Model>();
public constructor(private service: MyService){}
public toggle(): void {
this.service.send(model.id, model.name){
.subscribe(
result => this.onSuccess(result)),
error => this.onError(error),
() => this.onComplete());
}
public onSuccess(result: string): void {
if(result.inculdes("Some Text")) this.model.flag = true;
else this.model.flag = false;
//this.emitter.emit(this.model);
this.service.emitter.next(false);
}
public onError(error: any): void {
//notification using bootstrap-notify
}
public onComplete(): void {
//currently empty
}
}
Service
#Injectable // important
export class MyService{
public emitter: Subject<any> = new Subject();
public send(id: string, name: string){
return <Observable<string>>this.http
.post('url', new Dto(id, name))
.map(result => this.getData<string>(result))
.catch(this.catchBadResponse);
}
private getData<E>(result: Response): E {
//checking if result.status is ok
var body = result.json ? res.json(): null;
return <E>(body || {});
}
private catchBadRespomse: (error: any) => Observable<any> = (error: any) => {
var response = <Response>error;
var json = response.json();
var msg = json.Message;
var errormsg = json?
(json.error ? json.error: JSON.stringify(msg?msg:json)) :
(response.statusText || 'Error?');
return Obserable.of(errormsg);
}
}
Now you can listen to Service.emitter anywhere in app