I have a mat table in my angular project just like below:
The data is fetched from server using rest API. The server side is using nodejs and sequelize. When I click the delete button, I want my mat table to refresh with new data. But unfortunately, the refresh data is loaded before the delete is completed, so it looks as if the delete button is not functioning.
Here is my code:
list.component.ts
import {AfterViewInit, ChangeDetectorRef, Component, ViewChild} from '#angular/core';
import {MatPaginator, PageEvent} from '#angular/material/paginator';
import {MatSort} from '#angular/material/sort';
import {MatTableDataSource} from '#angular/material/table';
import { Title } from '#angular/platform-browser';
import { Router } from '#angular/router';
import { ClientService } from 'src/app/core/services/client.service';
import { MainService } from 'src/app/core/services/main.service';
import { Client } from 'src/app/models/client.model';
#Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ListComponent implements AfterViewInit {
clients: Client[];
displayedColumns: string[] = ['client_name', 'client_address', 'client_home_phone', 'client_mobile_phone', 'client_status', 'buttons'];
dataSource: MatTableDataSource<Client>;
length = 0;
#ViewChild(MatPaginator) paginator: MatPaginator;
#ViewChild(MatSort) sort: MatSort;
constructor(private changeDetectorRefs: ChangeDetectorRef, private router: Router, private titleService: Title, private mainService: MainService, private clientService: ClientService) {
this.titleService.setTitle('Clients');
this.mainService.setPageTitle('Clients');
this.mainService.setButtonToolbar([
{
'url': 'create',
'icon': 'add',
'color': 'primary'
},
]);
this.clientService.list(25, 0).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
});
}
ngAfterViewInit() {
}
onChangedPage(pageData: PageEvent) {
this.clientService.list(pageData.pageSize, pageData.pageIndex * pageData.pageSize).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
});
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.clientService.list(this.paginator.pageSize, 0, filterValue).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
});
}
onUpdate(id: String) {
this.router.navigate(['/client/update/'+id]);
}
onDelete(id: string) {
this.clientService.delete(id).subscribe(response => {
if (response == 200) {
this.clientService.list(this.paginator.pageSize, 0).subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
this.changeDetectorRefs.detectChanges();
});
}
});
}
}
client.service.ts
import { HttpClient, HttpHeaders } from "#angular/common/http";
import { Injectable } from "#angular/core";
import { map } from "rxjs/operators";
import { Client } from "src/app/models/client.model";
import { MainService } from 'src/app/core/services/main.service';
#Injectable()
export class ClientService {
constructor(private http: HttpClient, private mainService: MainService) {}
list(limit: number, offset: number, filter?: string) {
let queryParams = '?limit='+limit+'&offset='+offset;
if (filter) {
queryParams += '&filter='+filter;
}
return this.http
.get(this.mainService.getbaseApiUrl()+"client/list" + queryParams)
.pipe(map(responseData => {
const clientArray = [...responseData['rows']];
return {data: clientArray, count: responseData['count']};
}));
}
get(id: string) {
return this.http
.get(this.mainService.getbaseApiUrl()+"client/one/"+id);
}
add(client: Client) {
return this.http
.post(this.mainService.getbaseApiUrl()+'client/add', client, {
headers: new HttpHeaders({
// 'Origin': '*',
//'Access-Control-Allow-Origin': '*',
//'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json',
})
});
}
update(client: Client) {
return this.http
.post(this.mainService.getbaseApiUrl()+'client/edit', client, {
headers: new HttpHeaders({
// 'Access-Control-Allow-Origin': '*',
// 'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json',
})
});
}
delete(id: string) {
return this.http.delete(this.mainService.getbaseApiUrl()+'client/delete/'+id);
}
}
I'm new to Angular and this may look simple, but I couldn't think of any solution. Is there any way for Angular to wait until the deletion is completed then refresh? The temporary solution is to use setTimeout so the mat table will refresh after waiting for X seconds, but I don't think that's a clean solution.
Please help.
UPDATE:
My temporary solution is to reload the current router.
app-routing.module.ts
#NgModule({
imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})],
exports: [RouterModule]
})
list.component.ts
ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
}
}
onDelete(id: string) {
this.clientService.delete(id).subscribe(response => {
if (response == 200) {
this.router.navigate(['/client']);
}
});
}
But it will be great if there are other better solutions than this one.
onDelete(id: string) {
this.clientService.delete(id)
.pipe(switchMap(() => this.clientService.list(this.paginator.pageSize, 0)
.subscribe(clients => {
this.clients = clients.data;
this.dataSource = new MatTableDataSource(this.clients);
this.length = clients.count;
this.dataSource.sort = this.sort;
this.changeDetectorRefs.detectChanges();
});
Related
i have a messaging room application that create a discussion chat foreach room between users signed in with same room ,
i m facing this error :
Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.
i used 3 functions :
getChatMessages() : to get all chat messages from firestore for users
with same room
getCurrentRoom():to get the room of the connected user
getUsers(): return all users with same room
chat.services.ts
import { Injectable } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/compat/auth';
import { AngularFirestore } from '#angular/fire/compat/firestore';
import { Observable } from 'rxjs';
import { Timestamp } from 'rxjs/internal/operators/timestamp';
import { switchMap,map, timestamp, filter } from 'rxjs/operators';
import { query, orderBy, limit, where } from "firebase/firestore";
import firebase from 'firebase/compat/app';
export interface User {
uid: string;
email: string;
displayName:string;
username?:string;
room?:string
}
export interface Message {
createdAt: firebase.firestore.FieldValue;
id: string;
from: string;
msg: string;
fromName: string;
myMsg: boolean;
}
#Injectable({
providedIn: 'root'
})
export class ChatService {
currentUser: User ;
currentRoom:string="";
updatedRoom:string="";
constructor(private afAuth: AngularFireAuth, private afs: AngularFirestore) {
this.afAuth.onAuthStateChanged((user) => {
this.currentUser=user;
console.log("current email"+this.currentUser.email);
});
}
async signup({ username,email, password,room }): Promise<any> {
const credential = await this.afAuth.createUserWithEmailAndPassword(
email,
password
);
const uid = credential.user.uid;
return this.afs.doc(
`users/${uid}`
).set({
uid,
email: credential.user.email,
username:username,
room:room,
})
}
signIn({ email, password }) {
return this.afAuth.signInWithEmailAndPassword(email, password);
}
signOut(): Promise<void> {
return this.afAuth.signOut();
}
addChatMessage(msg) {
return this.afs.collection('messages').add({
createdAt:firebase.firestore.FieldValue.serverTimestamp(),//firebase.default.firestore.Timestamp,
msg: msg,
from: this.currentUser.uid
});
}
async getChatMessages() {
let users = [];
return (await this.getUsers()).pipe(
switchMap(res => {
users = res;
console.log("resssssss"+res);
return this.afs.collection('messages', ref => ref.orderBy('createdAt','asc')).valueChanges({ idField: 'id' }) as Observable<Message[]>;
}),
map(messages => {
console.log("messages"+messages);
// Get the real name for each user
for (let m of messages) {
m.fromName = this.getUserForMsg(m.from, users);
m.myMsg = this.currentUser.uid === m.from;
}
return messages
})
)
}
public async getCurrentRoom() {
await this.afs.collection('users', ref => ref.where("email", "==", this.currentUser.email)).get().toPromise()
.then(snapshot => {
snapshot.forEach(doc => {
this.currentRoom=JSON.parse(JSON.stringify(doc.data())).room;
console.log("current room" + this.currentRoom);
});
});
}
public async getUsers() {
console.log("this room" + this.currentRoom);
return this.afs.collection('users', ref => ref.where("room", "==", this.currentRoom)).valueChanges({
idField: 'uid'
}) as Observable < User[] > ;
}
private getUserForMsg(msgFromId, users: User[]): string {
for (let usr of users) {
if (usr.uid == msgFromId) {
return usr.username ?? 'undefind';
}
}
return 'Deleted';
}
}
my chat.page.ts :
import { Component, OnInit, ViewChild } from '#angular/core';
import { IonContent } from '#ionic/angular';
import { Observable } from 'rxjs';
import { ChatService } from '../chat.service';
import { Router } from '#angular/router';
import { AngularFireStorage, AngularFireUploadTask } from '#angular/fire/compat/storage';
import { AngularFirestore, AngularFirestoreCollection } from '#angular/fire/compat/firestore';
import { finalize, tap } from 'rxjs/operators';
export interface FILE {
name: string;
filepath: string;
size: number;
}
#Component({
selector: 'app-chat',
templateUrl: './chat.page.html',
styleUrls: ['./chat.page.scss'],
})
export class ChatPage implements OnInit {
ngFireUploadTask: AngularFireUploadTask;
progressNum: Observable<number>;
progressSnapshot: Observable<any>;
fileUploadedPath: Observable<string>;
room:any;
files: Observable<FILE[]>;
ImgtoSend:any;
FileName: string;
FileSize: number;
isImgUploading: boolean;
isImgUploaded: boolean;
private ngFirestoreCollection: AngularFirestoreCollection<FILE>;
#ViewChild(IonContent) content: IonContent;
messages:any=[];
newMsg = '';
constructor(private angularFirestore: AngularFirestore,
private angularFireStorage: AngularFireStorage,private chatService: ChatService, private router: Router) {
this.isImgUploading = false;
this.isImgUploaded = false;
this.ngFirestoreCollection = angularFirestore.collection<FILE>('filesCollection');
this.files = this.ngFirestoreCollection.valueChanges();
}
ngOnInit() {
this.messages= this.chatService.getChatMessages();
}
sendMessage() {
this.chatService.addChatMessage(this.newMsg).then(() => {
this.newMsg = '';
this.content.scrollToBottom();
});
}
signOut() {
this.chatService.signOut().then(() => {
this.router.navigateByUrl('/login', { replaceUrl: true });
});
}
fileUpload(event: FileList) {
const file = event.item(0)
if (file.type.split('/')[0] !== 'image') {
console.log('File type is not supported!')
return;
}
this.isImgUploading = true;
this.isImgUploaded = false;
this.FileName = file.name;
const fileStoragePath = `filesStorage/${new Date().getTime()}_${file.name}`;
console.log("filestoragepath"+fileStoragePath);
const imageRef = this.angularFireStorage.ref(fileStoragePath);
console.log("image ref"+imageRef);
this.ngFireUploadTask = this.angularFireStorage.upload(fileStoragePath, file);
this.ImgtoSend=this.FileName;
console.log("image to Send"+this.ImgtoSend);
this.progressNum = this.ngFireUploadTask.percentageChanges();
this.progressSnapshot = this.ngFireUploadTask.snapshotChanges().pipe(
finalize(() => {
this.fileUploadedPath = imageRef.getDownloadURL();
console.log("uploaded path"+this.fileUploadedPath);
this.fileUploadedPath.subscribe(resp=>{
this.fileStorage({
name: file.name,
filepath: resp,
size: this.FileSize
});
this.isImgUploading = false;
this.isImgUploaded = true;
},error => {
console.log(error);
})
}),
tap(snap => {
this.FileSize = snap.totalBytes;
})
)
}
fileStorage(image: FILE) {
const ImgId = this.angularFirestore.createId();
this.ngFirestoreCollection.doc(ImgId).set(image).then(data => {
console.log("data"+data);
}).catch(error => {
console.log(error);
});
}
}
enter code here
First, you have to eleminate all the code that is not relevant to the question to help people help you.
Second, the issue is simple:
What the compiler is telling you here is, Hey Mohammed Amir, *ngFor="" is used to loop through an Array of objects while you are passing to it an object literal.
Check the value of the property you bind to *ngFor="let msg of [YourAssumedArrayFromEndpointResponse]" in your template you will find that YourAssumedArrayFromEndpointResponse is not an array. That's why the compiler is complaining
I have started learning how to test angular projects. So far basic unit testing is working fine for me but for the dependency testing especially when API services are injected into the component I am facing issue for providing HttpClient. I have tried different solutions but none is working for me.
Service
// Present in HttpClientService file
getDisposition() {
return this.http.get<{ message: string, data: { dispositionList: Disposition[] } }>(`${this.URL}/disposition/get`);
}
// Present in FileProcessService file
deleteMedia(media: string) {
return this.http.delete<{ message: string }>(`${this.URL}/certificate/delete?certificate=${media}`);
}
add-edit-activity.component.ts
import { HttpEventType } from '#angular/common/http';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '#angular/core';
import { FormBuilder, Validators } from '#angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { DomSanitizer, SafeUrl } from '#angular/platform-browser';
import { AssignedPerson } from '#model/assigned-person.model';
import { Disposition } from '#model/disposition.model';
import { mimeTypes } from '#model/mime-type';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { DeleteComponent } from '#shared/delete/delete.component';
import { CustomErrorStateMatcher } from '#validator/error-state-matcher';
import { ToastrService } from 'ngx-toastr';
#Component({
selector: 'app-add-edit-activity',
templateUrl: './add-edit-activity.component.html',
styleUrls: ['./add-edit-activity.component.css']
})
export class AddEditActivityComponent implements OnInit {
constructor(private fb: FormBuilder, private sanitizer: DomSanitizer, private dialogRef: MatDialogRef<AddEditActivityComponent>, #Inject(MAT_DIALOG_DATA) public data: any,
private _http: HttpClientService, private _fileService: FileProcessService, private toastr: ToastrService, private dialog: MatDialog,) { }
re = new RegExp(/^[a-zA-Z-]*/, 'g');
ISOstamp = { T: ' ', Z: '000' };
matcher = new CustomErrorStateMatcher();
dispositionList: Disposition[] = [];
assignedPersonList: AssignedPerson[] = [];
filterAssignedPersonList: AssignedPerson[] = [];
uploaded = false;
uploadProgress = false;
fileURL: SafeUrl;
activityForm = this.fb.group({
gaugeId: [this.data.gaugeId, Validators.maxLength(9)], createTimeStamp: [{ value: new Date().toISOString().replace(/[TZ]/g, m => this.ISOstamp[m]), disabled: true }],
user: [{ value: sessionStorage.getItem('username'), disabled: true }], disposition: ['', [Validators.required, Validators.maxLength(30)]],
assignedPersonName: ['', Validators.maxLength(30)], department: ['', Validators.maxLength(20)],
shift: ['', Validators.maxLength(1)], remark: ['', Validators.maxLength(50)],
calibrationDate: ['', Validators.maxLength(10)], attachment: ['', Validators.maxLength(255)]
});
#ViewChild('file') certificate: ElementRef;
ngOnInit(): void {
this.data.type.match(this.re)[0] === 'Update' && this.setFormValues();
this._http.getDisposition().subscribe(response => this.dispositionList = response.data.dispositionList);
this._http.getAssignedPerson().subscribe(response => this.assignedPersonList = this.filterAssignedPersonList = response.data.assignedToList);
}
get GaugeId() {
return this.activityForm.get('gaugeId');
}
get TimeStamp() {
return this.activityForm.get('createTimeStamp');
}
get Attachment() {
return this.activityForm.get('attachment');
}
get Disposition() {
return this.activityForm.get('disposition');
}
get DispositionValue() {
return this.dispositionList.map(e => e.dispositionType).indexOf(this.Disposition.value) < 0;
}
get AssignedTo() {
return this.activityForm.get('assignedPersonName');
}
get AssignedToValue() {
return this.assignedPersonList.map(e => `${e.firstName} ${e.lastName}`).indexOf(this.Disposition.value) < 0;
}
private async setFormValues() {
this.activityForm.patchValue({ ...this.data });
if (this.data.attachment) {
this.uploadProgress = true;
this.uploaded = true;
await this.fetchUploadedFile(this.data.attachment, mimeTypes[this.data.attachment.split('.')[1]]);
this.activityForm.markAsPristine();
}
}
searchAssignedPerson(event) {
if (event.target.value) {
this.filterAssignedPersonList = [];
for (let person of this.assignedPersonList) {
if (person.firstName.toLowerCase().startsWith(event.target.value.toLowerCase())) {
this.filterAssignedPersonList.push(person);
}
}
} else { this.filterAssignedPersonList = this.assignedPersonList }
}
upload(event) {
const file: File = event.target.files[0];
this.certificate.nativeElement.value = '';
if (file.size > (20 * 1000 * 1000)) { // Checking if File size is above 20MB
this.toastr.error('Size of ' + file.name + ' is above 20MB');
return;
}
const fd = new FormData();
fd.append('certificate', file, file.name);
this.processAttachment(fd);
}
private processAttachment(file: FormData) {
this._fileService.uploadMedia(file).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) { this.uploadProgress = true }
if (event.type === HttpEventType.Response) {
let media = event.body.data.Certificate;
this.fetchUploadedFile(media.fileName, media.fileType);
this.toastr.info(event.body.message);
}
}, error => {
this.toastr.error(error.error.message);
this.uploadProgress = false;
});
}
private async fetchUploadedFile(file: string, mimeType: string) {
try {
let response = await this._fileService.getMedia(file).toPromise();
this.fileURL = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(new Blob([response], { type: mimeType })));
this.uploaded = true;
this.uploadProgress = false;
this.activityForm.patchValue({ attachment: file });
this.Attachment.markAsDirty();
} catch (error) {
this.uploadProgress = false;
this._fileService.processError(error.error)
}
}
deleteFile() {
this.dialog.open(DeleteComponent, {
width: '350px', disableClose: true
}).afterClosed().subscribe(response => {
if (response) {
this._fileService.deleteMedia(this.Attachment.value).subscribe(response => {
this.toastr.info(response.message);
this.activityForm.patchValue({ attachment: '' });
this.Attachment.markAsDirty();
this.uploaded = false;
}, error => this.toastr.error(error.error.message));
}
});
}
async doAction() {
let message = '';
if (this.data.type.match(this.re)[0] === 'Add') {
message = await (await this._http.addActivityLog(this.activityForm.getRawValue()).toPromise()).message;
} else {
message = await (await this._http.updateActivityLog(this.GaugeId.value, this.TimeStamp.value, this.activityForm.getRawValue()).toPromise()).message;
}
this.dialogRef.close(message);
}
}
add-edit-activity.component.spec.ts
import { ComponentFixture, TestBed, tick } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let _http: HttpClientService, _file: FileProcessService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },,
{ provider: HttpClientService, useValue: null },
{ provider: FileProcessService, useValue: null }
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
_http = fixture.debugElement.injector.get(HttpClientService);
_file = fixture.debugElement.injector.get(FileProcessService);
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
let spy = spyOn(_http, 'getDisposition').and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
let spy = spyOn(_file, 'deleteMedia').and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});
The error that am getting for those 2 tests (I'm providing the error of 1 test case, the same is coming for the 2nd one also):
SHOULD mock http service
AddEditActivityComponent
Error: Invalid provider for the NgModule 'DynamicTestModule' - only instances of Provider and Type are allowed, got: [..., ..., ...,
..., ?undefined?, ..., ...]
at throwInvalidProviderError (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:240:1)
at providerToFactory (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11550:1)
at providerToRecord (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11521:1)
at R3Injector.processProvider (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11424:1)
Error: : could not find an object to spy upon for getDisposition() Usage: spyOn(, ) Error: :
could not find an object to spy upon for getDisposition() Usage:
spyOn(, )
at
Anyone, please help what's the exact error happening with my test cases? Stuck here for almost 2 days.
You have to avoid using a 'null' value when providing services in the TestBed, that what is causing your problem.
The best way to mock Angular services is to create SpyObj instances and then use them like what is done Angular tests documentation.
Here is how would be your spec file if you were using SpyObj:
import { ComponentFixture, TestBed, tick } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let httpSpy: jasmine.SpyObj<HttpClientService>;
let fileSpy: jasmine.SpyObj<FileProcessService>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },
{
provider: HttpClientService,
useValue: jasmine.createSpyObj('HttpClientService', ['getDisposition', 'getAssignedPerson', 'addActivityLog', 'updateActivityLog'])
},
{
provider: FileProcessService,
useValue: jasmine.createSpyObj('FileProcessService', ['uploadMedia', 'getMedia', 'deleteMedia', 'processError'])
}
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
httpSpy = fixture.debugElement.injector.get(HttpClientService) as jasmine.SpyObj<HttpClientService>;
fileSpy = fixture.debugElement.injector.get(FileProcessService) as jasmine.SpyObj<FileProcessService>;
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
httpSpy.getDisposition.and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
fileSpy.deleteMedia.and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});
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.
when trying to cancel upload by unsubscribing what actually happen that i unsubscribe to upload progress but that actual upload is not cancelled and keep uploading to the server.
upload.components.ts
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '#angular/core';
import { Subject, Subscription, Observable } from 'rxjs';
import { HttpEventType } from '#angular/common/http';
import { UploadService } from '../../../services';
import { takeUntil } from 'rxjs/operators';
#Component({
selector: 'app-image-upload-item',
templateUrl: './image-upload-item.component.html',
styleUrls: ['./image-upload-item.component.scss']
})
export class ImageUploadItemComponent implements OnInit, OnDestroy {
#Input() index: any;
#Output() uploadSuccess: EventEmitter<any>;
#Output() uploadCanceled: EventEmitter<any>;
public localimageURL: string;
public uploadProgress: number;
public isUploadCompleted: boolean;
public uploadImageObservable: Subscription;
public isReadyForUpload: boolean;
public isUploading: boolean;
public progressMode: string;
public readonly unique: string = Math.floor((Math.random() *
100)).toString();
public readonly imagePreviewID = 'imagePreview' + this.unique;
_file: any;
#Input() public set file(value: any) {
const reader = new FileReader();
reader.onload = (e: any) => {
this.localimageURL = e.target.result;
};
this._file = value;
reader.readAsDataURL(this._file);
console.log(this._file);
}
constructor(private uploadService: UploadService) {
this.uploadProgress = 0;
this.isUploading = false;
this.localimageURL = '';
this.isUploadCompleted = false;
this.uploadSuccess = new EventEmitter<any>();
this.uploadCanceled = new EventEmitter<any>();
this.progressMode = 'indeterminate';
}
ngOnInit() {
this.uploadImageToServer(this._file);
// setTimeout(() => {
// console.log('im in set time out unsubscripting',
this.uploadImageObservable);
// this.uploadImageObservable.forEach(subscription => {
// subscription.unsubscribe();
// });
// }, 100);
}
ngOnDestroy() {
console.log('component destroyed');
this.uploadImageObservable.unsubscribe();
}
public clearUploadButtonClicked() {
// if (this.uploadImageObservable !== undefined) {
// console.log('image observable is defined');
// this.uploadImageObservable.unsubscribe();
// console.log(this.uploadImageObservable.closed);
// }
// this.uploadImageObservable.unsubscribe();
this._file = '';
this.uploadCanceled.emit({ index: this.index, uploaded: false });
}
public get showUploadProgress(): boolean {
return this.uploadProgress !== 0;
}
public uploadImageToServer(file) {
this.isUploading = true;
const progress = new Subject<number>();
progress.subscribe(value => {
this.uploadProgress = value;
});
this.uploadImageObservable = this.uploadService.uploadImage(file)
.subscribe(result => {
const type = result.type;
const data = result.data;
console.log(result);
if (type === HttpEventType.UploadProgress) {
const percentDone = Math.round(100 * data.loaded / data.total);
progress.next(percentDone);
if (percentDone === 100) {
this.progressMode = 'indeterminate';
}
} else if (type === HttpEventType.Response) {
if (data) {
progress.complete();
this.progressMode = 'determinate';
this.isReadyForUpload = false;
this.isUploadCompleted = true;
this.isUploading = false;
this.uploadSuccess.emit({ index: this.index, mediaItem: data });
}
}
}, errorEvent => {
});
}
}
upload.service.ts
public uploadImage(imageFile: File): Observable<any> {
const formData: FormData = new FormData();
if (imageFile !== undefined) {
formData.append('image', imageFile, imageFile.name);
const req = new HttpRequest('POST', environment.uploadImageEndPoint,
formData, {
reportProgress: true,
});
return new Observable<any>(observer => {
this.httpClient.request<any>(req).subscribe(event => {
if (event.type === HttpEventType.Response) {
const responseBody = event.body;
if (responseBody) {
this.alertService.success(responseBody.message);
observer.next({ type: event.type, data: new
MediaItem(responseBody.mediaItem) });
}
} else if (event.type === HttpEventType.UploadProgress) {
observer.next({ type: event.type, data: { loaded: event.loaded, total:
event.total } });
} else {
observer.next(event);
}
}, errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
}
observer.next(null);
});
});
}
}
how can i cancel upload request properly with observable unsubscribe
note i already tried pipe takeuntil() and nothing changed
What you'll want to do is return the result from the pipe function on the http request return observable. Right now you have multiple streams and the component's unsubscribe is only unsubscribing to the observable wrapping the http request observable (not connected).
You'll want to do something like:
return this.httpClient.request<any>(req).pipe(
// use rxjs operators here
);
You'll then use rxjs operators (I've been doing this for a while, but I still highly reference this site) to perform any logic needed and reflect things like your errors and upload progress to the component calling the service. On the component side, you'll keep your subscribe/unsubscribe logic.
For instance, you can use the switchMap operator to transform what is returning to the component from the http request observable and specify the value to return to the component, and catchError to react to any errors accordingly.
return this.httpClient.request<any>(req).pipe(
switchMap(event => {
if (event.type === HttpEventType.Response) {
const responseBody = event.body;
if (responseBody) {
this.alertService.success(responseBody.message);
return { type: event.type, data: new MediaItem(responseBody.mediaItem) };
}
} else if (event.type === HttpEventType.UploadProgress) {
return { type: event.type, data: { loaded: event.loaded, total: event.total } };
}
return event;
}),
catchError(errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
}
return of(<falsy or error value>);
}),
);
Alternatively you could model it a little more after this example by just returning the http function call from the service to the component and handling things in the subscribe there.
actually i found a way as follows
public uploadImage(imageFile: File): Observable<any> {
const formData: FormData = new FormData();
if (imageFile !== undefined) {
formData.append('image', imageFile, imageFile.name);
const req = new HttpRequest('POST', environment.uploadImageEndPoint, formData, {
reportProgress: true,
});
return this.httpClient.request<any>(req).pipe(
map((res: any) => {
return res;
}),
catchError(errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
return Observable.throw(errorEvent);
}
return Observable.throw(errorEvent);
}));
}
}
I've got a service, which determines the location, it's written as Observable
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
const GEOLOCATION_ERRORS = {
'errors.location.unsupportedBrowser': 'Browser does not support location services',
'errors.location.permissionDenied': 'You have rejected access to your location',
'errors.location.positionUnavailable': 'Unable to determine your location',
'errors.location.timeout': 'Service timeout has been reached'
};
#Injectable()
export class GeolocationService {
public getLocation(opts): Observable<any> {
return Observable.create(observer => {
if (window.navigator && window.navigator.geolocation) {
window.navigator.geolocation.getCurrentPosition(
(position) => {
observer.next(position);
observer.complete();
},
(error) => {
switch (error.code) {
case 1:
observer.error(GEOLOCATION_ERRORS['errors.location.permissionDenied']);
break;
case 2:
observer.error(GEOLOCATION_ERRORS['errors.location.positionUnavailable']);
break;
case 3:
observer.error(GEOLOCATION_ERRORS['errors.location.timeout']);
break;
}
}, opts);
} else {
observer.error(GEOLOCATION_ERRORS['errors.location.unsupportedBrowser']);
}
});
}
}
export var GeolocationServiceInjectables: Array<any> = [
{ provide: GeolocationService, useClass: GeolocationService }
];
Then in my HttpService I want to construct the query URL with the output from location service
import { Observable } from 'rxjs/Observable';
import { Injectable, Inject } from '#angular/core';
import { Http, Response } from '#angular/http';
import { GeolocationService } from './location.service';
import { WeatherItem } from '../weather-item/weather-item.model';
export const OpenWeatherMap_API_KEY: string = 'SOME_API_KEY';
export const OpenWeatherMap_API_URL: string = 'http://api.openweathermap.org/data/2.5/forecast';
#Injectable()
export class HttpService {
constructor(private http: Http,
private geolocation: GeolocationService,
#Inject(OpenWeatherMap_API_KEY) private apiKey: string,
#Inject(OpenWeatherMap_API_URL) private apiUrl: string) {
}
prepaireQuery(): void {
this.geolocation.getLocation({ enableHighAccuracy: false, maximumAge: 3 }).subscribe(
(position) => {
let params: string = [
`lat=${position.latitude}`,
`lon=${position.longitude}`,
`APPID=${this.apiKey}`,
].join('&');
// return `${this.apiUrl}?${params}`;
}
);
}
getWeather(): Observable<WeatherItem[]> {
return this.http.get(/*there should be the url*/)
.map((response: Response) => {
return (<any>response.json()).items.map(item => {
const city = {
city: item.city.name,
country: item.city.country,
}
return item.list.map(entity => {
return new WeatherItem({
temp: entity.main.temp,
temMin: entity.main.temp_min,
temMax: entity.main.temp_max,
weatherCond: entity.weather.main,
description: entity.weather.description,
windSpeed: entity.wind.speed,
icon: entity.weather.icon,
city,
})
})
})
})
}
}
export var HttpServiceInjectables: Array<any> = [
{ provide: HttpService, useClass: HttpService },
{ provide: OpenWeatherMap_API_KEY, useValue: OpenWeatherMap_API_KEY },
{ provide: OpenWeatherMap_API_URL, useValue: OpenWeatherMap_API_KEY }
];
The question is how to get the URL before doing request. I've seen solutions with unsubscribe(), but I think thay are not so good. I've thought about merge() but I'm not sure that it's what I really want.
You are probably looking for the mergeMap operator of RxJs.
What mergeMap does is automatically subscribes to the source observable, then lets you work with its result in your inner observable, and then finally flattens your output.
In this example, you call the firstUrl and use the result you get from that request in your second call to secondUrl:
this.http.get(`{firstUrl}`)
.mergeMap(res => this.http.get(`{secondUrl}/{res.json()}`))
.subscribe(...)
I have not made it specific for your code, as I'm not sure of exactly what you want to do. But I hope this will help you on the way!
This can be done using map/flatMap combination:
getWeather(): Observable<WeatherItem[]> {
return this.geolocation.getLocation({ enableHighAccuracy: false, maximumAge: 3 })
.map((position) => {
let params: string = [
`lat=${position.latitude}`,
`lon=${position.longitude}`,
`APPID=${this.apiKey}`,
].join('&');
return `${this.apiUrl}?${params}`;
})
.flatMap(url => this.http.get(url)
.map((response: Response) => {
return (<any>response.json()).items.map(item => {
const city = {
city: item.city.name,
country: item.city.country,
}
return item.list.map(entity => {
return new WeatherItem({
temp: entity.main.temp,
temMin: entity.main.temp_min,
temMax: entity.main.temp_max,
weatherCond: entity.weather.main,
description: entity.weather.description,
windSpeed: entity.wind.speed,
icon: entity.weather.icon,
city,
})
})
})
})
}