Create array of object from 3 arrays of objects - javascript

I have 3 private methods in the angular component which return arrays of objects.
I need to create one array of the object which will contain all 3. All of them has the same class
Here is it
export class TimelineItemDto {
id: any;
creatorAvatarUrl: string;
categoryName: string;
creatorName: string;
subcategoryName: string;
description: string;
type: string;
}
Here is the code of the component
export class HomeComponent implements OnInit {
constructor(private _router: Router, private http: HttpClient) { }
color: ThemePalette = 'primary';
classes: TimelineItemDto[] = [];
requests: TimelineItemDto[] = [];
courses: TimelineItemDto[] = [];
timelineItems: TimelineItemDto[] = [];
checked = false;
disabled = false;
ngOnInit(): void {
const token = localStorage.getItem('jwt');
if (!token) {
this._router.navigate(['/main/login']);
}
this.getTimelineItems();
}
getCourses(): any {
return this.http
.get(environment.baseUrl + '/Course/GetCourses')
.subscribe((data: TimelineItemDto[]) => {
return data;
});
}
getClasses(): any {
return this.http
.get(environment.baseUrl + '/Class/GetClasses')
.subscribe((data: TimelineItemDto[]) => {
return data;
});
}
getRequest(): any {
return this.http
.get(environment.baseUrl + '/Requests/GetRequests')
.subscribe((data: TimelineItemDto[]) => {
return data;
});
}
getTimelineItems(): any {
var courses = this.getCourses();
var classes = this.getClasses();
var requests = this.getRequest();
this.timelineItems = [...classes, ...courses, ...requests];
console.log(this.timelineItems);
}
}
At this row this.timelineItems = [...classes, ...courses, ...requests]; I have this error
core.js:4197 ERROR TypeError: classes is not iterable
How I can fix this?

The Problem
Consider the code below
getCourses(): any {
return this.http
.get(environment.baseUrl + '/Course/GetCourses')
.subscribe((data: TimelineItemDto[]) => {
return data;
});
}
The above code calls .get() method then calls .subscription() method. This indicates that this method actually returns a subscription and NOT an Observable. As the error indicates you are trying to iterate over these subscription hence the error
Solution
To solve this, there are various ways, my approach will be as below
get classes as Observable
get requests as Observable
get courses as Observable
combine these 3 Observables to one Observable
subscribe to the new Observable
See Below code
constructor(private _router: Router, private http: HttpClient) {}
color: ThemePalette = "primary";
timelineItems: TimelineItemDto[] = []
getCourses = () =>
this.http.get<TimelineItemDto[]>(
environment.baseUrl + "/Course/GetCourses"
);
getClasses = () =>
this.http.get<TimelineItemDto[]>(environment.baseUrl + "/Class/GetClasses");
getRequest = () =>
this.http.get<TimelineItemDto[]>(
environment.baseUrl + "/Requests/GetRequests"
);
classes$: Observable<TimelineItemDto[]> = this.getClasses();
requests$: Observable<TimelineItemDto[]> = this.getRequest();
courses$: Observable<TimelineItemDto[]> = this.getCourses();
timelineItems$: Observable<TimelineItemDto[]> = combineLatest([
this.classes$,
this.courses$,
this.requests$
]).pipe(
map(([classes, courses, requests]) => [...classes, ...courses, ...requests])
);
checked = false;
disabled = false;
ngOnInit(): void {
const token = localStorage.getItem("jwt");
if (!token) {
this._router.navigate(["/main/login"]);
}
this.getTimelineItems();
}
getTimelineItems(): any {
this.timelineItems$.subscribe({
next: (items) => this.timelineItems = items
})
}
See this solution on stackblitz

That isn't how asynchronous data works. Please refer here for more info on async data.
In short, you need to wait till the data is emitted by the source. In this specific case, you need to wait for the RxJS observables emit the values before trying to assign them. And seeing that you need to subscribe to multiple observables, you could use RxJS forkJoin function to trigger the requests in parallel
export class HomeComponent implements OnInit {
constructor(private _router: Router, private http: HttpClient) { }
color: ThemePalette = 'primary';
classes: TimelineItemDto[] = [];
requests: TimelineItemDto[] = [];
courses: TimelineItemDto[] = [];
timelineItems: TimelineItemDto[] = [];
checked = false;
disabled = false;
ngOnInit(): void {
const token = localStorage.getItem('jwt');
if (!token) {
this._router.navigate(['/main/login']);
}
this.getTimelineItems();
}
getTimelineItems(): any {
forkJoin(
<Observable<TimelineItemDto[]>>this.http.get(environment.baseUrl + '/Class/GetClasses'),
<Observable<TimelineItemDto[]>>this.http.get(environment.baseUrl + '/Course/GetCourses'),
<Observable<TimelineItemDto[]>>this.http.get(environment.baseUrl + '/Requests/GetRequests')
).subscribe({
next: ([classes, courses, requests]) => {
this.classes = classes;
this.courses = courses;
this.requests = requests;
this.timelineItems = [...classes, ...courses, ...requests];
console.log(this.timelineItems);
},
error: error => {
console.log('handle error');
}
});
}
}
Please go through the link above. The variable this.timelineItems might still be empty when you try to access it outside the subscription as it might not yet be assigned the values.
In other words, the this.timelineItems would only be properly accessible inside the subscription.

Related

How to return an Observable Array instead of a subscription?

How can I make this forkJoin return an observable-array instead of a subscription?
connect(): Observable<any[]> {
this.userId = this.authService.userId;
this.habits$ = this.habitService.fetchAllById(this.userId);
this.status$ = this.statusService.fetchAll();
this.joined$ = forkJoin([
this.habits$,
this.status$
]).subscribe(([habits, statuses]) => {
this.joined = habits.map(habit => ({
...statuses.find(t => t.habitId === habit.habitId),
...habits
}));
});
return this.joined$;
}
At the moment my variables are defined like this:
export class HabitDataSource extends DataSource<any> {
userId: Pick<User, 'id'>;
habits$: Observable<Habit[]>;
status$: Observable<Status[]>;
joined$: Subscription;
joined: any[];
But the connect() method needs an Observable-Array.
How can I make this forkJoin return an observable-array instead of a subscription?
forkJoin DOES return array. The reason you get a Subscription is because you are calling .subscribe().
instead of doing your mapping logic inside subscribe, you can use the map operator:
connect(): Observable<any[]> {
this.userId = this.authService.userId;
this.habits$ = this.habitService.fetchAllById(this.userId);
this.status$ = this.statusService.fetchAll();
return forkJoin([this.habits$, this.status$]).pipe(
map(([habits, statuses]) =>
habits.map(habit => ({
...habit,
status: statuses.find(s => s.habitId === habit.habitId)
}))
)
);
}

Angular *ngFor not getting previous elements

I am using socket.io in my angular and node application. A user joins the room the user can see his username in the user list. When user2 joins, user1 can see both user1 and user2 in the user list. However, user2 can only see user2. If user 3 joins. user1 can see user 1, user2, and user3. User2 can see user2 and user3. However, user3 only sees user3.
chat.service.ts
import { Injectable } from '#angular/core';
import * as io from 'socket.io-client';
import { Observable, onErrorResumeNext, observable } from 'rxjs';
//import { Observable } from 'rxjs/Observable';
#Injectable()
export class ChatService {
private socket = io('http://localhost:8080');
joinRoom(data) {
this.socket.emit('join', data);
}
newUserJoined() {
let observable = new Observable<{user: String, message:String}>(observer => {
this.socket.on('new user joined ', (data) => {
observer.next(data);
});
return () => {
this.socket.disconnect();
};
});
return observable;
}
leaveRoom(data) {
this.socket.emit('leave', data);
}
userLeftRoom() {
let observable = new Observable<{user: String, message:String}>(observer => {
this.socket.on('left room', (data) => {
observer.next(data);
});
return () => {
this.socket.disconnect();
};
});
return observable;
}
sendMessage(data) {
this.socket.emit('message', data);
}
newMessageRecieved() {
let observable = new Observable<{user: String, message:String, time: any}>(observer => {
this.socket.on('new message', (data) => {
observer.next(data);
});
return () => {
this.socket.disconnect();
};
});
return observable;
}
getRoomUsers() {
let observable = new Observable<{user: String, message:String}>(observer => {
this.socket.on('roomUsers', (data) => {
observer.next(data);
});
return () => {
this.socket.disconnect();
};
});
return observable;
}
}
chat.component.ts
import { Component, OnInit } from '#angular/core';
import { ChatService } from '../../services/chat.service';
import { AuthService } from '../../services/auth.service';
import { Observable } from 'rxjs';
#Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.css'],
providers: [ChatService]
})
export class ChatComponent implements OnInit {
room: any;
user: any;
username: any;
roomName: any;
messageArray: Array<{user: String, message: String, time: any}> = [];
userArray: Array<{user: String, message: String}> = [];
messageText: String;
time: any;
constructor( private chatService: ChatService, private authService: AuthService) {
// this.chatService.newUserJoined()
// .subscribe(data => this.userArray.push(data));
// this.chatService.userLeftRoom()
// .subscribe(data => this.userArray.splice(this.userArray.indexOf(data)));
this.chatService.newMessageRecieved()
.subscribe(data => this.messageArray.push(data));
this.chatService.getRoomUsers()
.subscribe(data => this.userArray.push(data));
}
ngOnInit() {
this.getUser();
}
getUser() {
this.user = localStorage.getItem('user');
this.username = JSON.parse(this.user).username;
this.getRoom();
}
getRoom() {
this.room = localStorage.getItem('room');
this.roomName = JSON.parse(this.room).name;
this.join();
}
join() {
console.log(this.roomName);
console.log(this.username);
this.chatService.joinRoom({user: this.username, room: this.roomName});
}
leave() {
console.log(this.roomName);
console.log(this.username);
let userIndex = this.userArray.indexOf(this.username);
delete this.userArray[userIndex];
localStorage.removeItem('room');
this.chatService.leaveRoom({user: this.username, room: this.roomName});
}
sendMessage() {
console.log(this.roomName);
console.log(this.username);
this.chatService.sendMessage({user: this.username, room: this.roomName, message: this.messageText, time: this.time});
this.messageText = '';
}
}
chat.component.html
<ul *ngFor="let item of userArray" id="usersList">
<li >{{item.user}}</li>
</ul>
What comes through the “roomUsers” socket event? All the current users?
To me it looks like that event is only dispatching new arrivals, so every user that shows up only sees themselves and users that come after them.
Without too much knowledge of your code, it seems like one solution is to make the “roomUsers” event contain all users and not just newly added users. Another possibility is to define that Observable outside of the function and then provide the same observable to each newcomer, so everyone has the same data. Switch that function to a stream and expose it something like:
roomUsers$ = from(this.socket.on(“roomUsers”).pipe(
scan((a,c) => [...a, c], []),
shareReplay(1));
The shareReplay(1) operator means even late subscribers get the most recent value, and the scan builds things up as an array. So on your component you wouldn’t do the .push method into the array.
Again, I could be missing context, but hopefully this points you in the right directions.

Websocket event is being catched only by one component

I have a websocket service in Angular7
import { environment } from './../../../environments/environment.prod';
import { Injectable } from '#angular/core';
import * as Rx from 'rxjs';
import {map} from 'rxjs/operators';
#Injectable()
export class WsService{
public wsMessages: Rx.Subject<any>;
public connect(url): Rx.Subject<MessageEvent> {
if(!this.subject){
this.subject = this.create(url);
console.log("Websocket (Dashboard) successfully connected to : ", url);
}
return this.subject;
}
private create(url): Rx.Subject<MessageEvent> {
let ws = new WebSocket(
url,
[`Bearer`, `${this.token.substring(7)}`]
);
let observable = Rx.Observable.create(
(obs: Rx.Observer<MessageEvent>) => {
ws.onmessage = obs.next.bind(obs);
ws.onerror = obs.error.bind(obs);
ws.onclose = obs.complete.bind(obs);
return ws.close.bind(ws);
}
)
let observer = {
next: (data: Object) => {
if(ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
}
return Rx.Subject.create(observer, observable);
}
private token: string;
constructor(){
this.token = JSON.parse(localStorage.getItem('user')).token
this.wsMessages = <Rx.Subject<any>>
this.connect(`${environment.websocketUrl}/dashboard/ws`)
.pipe(
map((response: MessageEvent): any =>{
let data = JSON.parse(response.data);
return data;
})
)
}
private subject: Rx.Subject<MessageEvent>;
}
and i have mulltiple component that subscribe to wsMessages
this.ws.wsMessages.subscribe(msg => {
this.catchWebSocketEvents(msg)
console.log("LeftBarSocket : ", msg);
})
the events are only printed on one component only and i need many components to listen to those ws events.
Ok so after doing a bit of reading on the subject, I found out there is a share() function that allows multiple subscribers sharing a source.
the solution is here :
const observable = Rx.Observable.create(
(obs: Rx.Observer<MessageEvent>) => {
this.ws.onmessage = obs.next.bind(obs);
this.ws.onerror = obs.error.bind(obs);
this.ws.onclose = obs.complete.bind(obs);
return this.ws.close.bind(this.ws);
}
).pipe(
share()
)
just need to add the .pipe(share()) to the observable, being created.

Rxjs issue with the concat operator when executing operations sequentially

I am using rxjs 6 and I am executing two async operations where the order is important.
I do have this chunk of code which works perfectly:
dbmsProxy.createDatastores().subscribe(() => {
UsersDAO.insert(users).subscribe(() => {
console.log('FINISHED ALL THE CHAIN');
});
});
But when I try to use concat of rxjs I do have an issue because the second one is executed before the first one finishes:
concat([dbmsProxy.createDatastores(), UsersDAO.insert(users)]).subscribe();
Below the DBMSProxy methods
public createDatastores(): Observable<string> {
const _this: DBMSProxy = this;
const subject = new Subject<string>();
const subscription: Subscription = UsersDAO.createDatastore().subscribe(
onSuccess,
onError,
onFinally
);
return subject;
function onSuccess(datastore: Nedb): void {
console.log(`USERS Datastore Created Successfully`);
_this.db.users = datastore;
subject.next('success');
}
function onError(err: string) {
subject.error('error');
console.error(err);
}
function onFinally() {
subject.complete();
subscription.unsubscribe();
}
}
public insertDocuments(documents: any, datastore: Nedb): Subject<any> {
const subject = new Subject<any>();
datastore.insert(documents, onInsert);
return subject;
function onInsert(err: Error, newDocuments: any) {
if (err) {
subject.error(err);
} else {
// add to the documents to insert the id just created from nedb when inserting the document
documents.forEach((document: any, ind: number) => {
document.id = newDocuments[ind]._id;
});
subject.next(documents);
}
subject.complete();
}
}
And below the UsersDAO methods:
public static createDatastore(): Subject<Nedb | string> {
const subject = new Subject<Nedb | string>();
const datastore = new Nedb({
filename: USERS_DATASTORE_FULL_NAME,
autoload: true,
onload
});
return subject;
function onload(err: Error) {
if (err) {
subject.error(
`Error creating USERS datastore: ${err.name} - ${err.message}`
);
} else {
subject.next(datastore);
}
subject.complete();
}
}
public static insert(users: User[]): Observable<any> {
return DBMSProxy.getInstance()
.insertDocuments(users, DBMSProxy.getInstance().db.users)
.pipe(catchError((val: any) => of('Error inserting the users')));
}
Any idea of what's going on please?
My current solution is to convert the Subject to Observable, create a new Observable with the second one, and remove the square brackets (otherwise I will get back the observables and not the results) and this seems to work:
const operations = concat(
dbmsProxy.createDatastores().asObservable(),
defer(() => UsersDAO.insert(users))
);
operations.subscribe(onSubscribe);
function onSubscribe(result: any) {
console.log('Finished all: ', result);
}

Angular 5 / Typescript - How to cast complex json object to a class

I'm trying to cast a complex (multiple type classes) json response object ( which i receive from my nodejs/mongoose backend) to a typescript class.
A moment class contains an author of type user and a comments array of type comment.
moment.model.ts
import { Comment } from './comment.model';
import { User } from './user.model';
export class Moment {
_id?: string = null;
body?: string = null;
_author?: User = null;
likes?: any[] = [];
dislikes?: any[] = [];
_comments?: Comment[] = [];
created_at?: string = null;
updated_at?: string = null;
constructor(data?: Moment) {
console.log(data);
if (data) {
this.deserialize(data);
}
}
private deserialize(data: Moment) {
const keys = Object.keys(this);
for (const key of keys) {
if (data.hasOwnProperty(key)) {
this[key] = data[key];
}
}
}
public get author(): User {
return this._author;
}
public set author(data: User) {
this._author = new User(data);
}
public get comments(): Comment[] {
return this._comments;
}
public set comments(data: Comment[]) {
this._comments = data.map(c => new Comment(c));
}
}
comment.model.ts
export class Comment {
_id?: string = null;
body?: string = null;
moment?: any = null;
author?: any = null;
likes?: any[] = [];
dislikes?: any[] = [];
parent?: any = null;
replies?: any = null;
updated_at?: string = null;
created_at?: string = null;
constructor(data?: Comment) {
console.log(data);
if (data) {
this.deserialize(data);
}
}
private deserialize(data: Comment) {
const keys = Object.keys(this);
for (const key of keys) {
if (data.hasOwnProperty(key)) {
this[key] = data[key];
}
}
}
}
user.model.ts
export class User {
_id?: string = null
updated_at?: string = null;
created_at?: string = null;
profile?: any = null;
phone?: any = null;
email?: any = null;
followers: any[] = [];
following: any[] = [];
isOnline: any = null;
socketId: any = null;
constructor(data?: User) {
console.log(data);
if (data) {
this.deserialize(data);
}
}
private deserialize(data: User) {
const keys = Object.keys(this);
for (const key of keys) {
if (data.hasOwnProperty(key)) {
this[key] = data[key];
}
}
}
moment.service.ts
get(moment_id) {
let endpoint = this.path + moment_id;
return this.apiService.get(endpoint)
.map((res) => new Moment(res.data));
}
moment-detail.component.ts
this.route.params.switchMap((params) => {
let moment_id = params['id'];
return this.momentService.get(moment_id);
}).subscribe((res) => {
this.moment = res;
console.log(this.moment);
});
When i call my service i assign the json to a new class of Moment. In the component i then try to print this.moment. Everything is fine except for the author and a comments which are null/empty.
Your deserialize method in Moment is treating every field the same way and just copying the value from the JSON. This means it never constructs User or Comment objects. You should probably write it "by hand" to deal with each property correct, instead of using a loop.
You can try something like
this[key]=( key=='_author' ? new User(data[key]):(key=='_comments'?new Comment(data[key]): data[key]))
Or
if(key=='_author')
this[key]=new User(data[key]);
else if(key=='_comments')
this[key]=new Comment(data[key]);
else
this[key]=data[key]

Categories

Resources