Strange behaviour of event emitted by child component to parent component - html

I am emitting array of URLimages from child component to parent using EventEmitter.
Child's emitter:
#Output() images = new EventEmitter<string[]>();
Parent's html:
<app-file-upload (images)="onLoadedImages($event)"></app-file-upload>
where <app-file-upload> is the child.
Parent's OnLoadedImages() function:
onLoadedImages(images: string[]) {
console.log(images);
console.log(images[0]);
}
Console output:
Why does images[0] give undefined when in the console output I can see it has the data and how can I access images: string[] data?
Edit:
#Colby Hunter As an answer to comment, here's content of child:
#Component({
selector: 'app-file-upload',
templateUrl: './file-upload.component.html',
styleUrls: ['./file-upload.component.css']
})
export class FileUploadComponent implements OnInit {
loadedImagesAsURL = [];
#Output() images = new EventEmitter<string[]>();
constructor() {
}
ngOnInit() {
}
onFileSelected(event) {
const filesList = event.target.files;
for (const file of filesList) {
const reader = new FileReader();
reader.onload = (e: any) => {
this.loadedImagesAsURL.push(e.target.result);
};
reader.readAsDataURL(file);
}
this.images.emit(this.loadedImagesAsURL);
}
}
<div class="text-center" *ngIf="loadedImagesAsURL.length>0" style="height: 300px; overflow: auto;">
<span *ngFor="let image of loadedImagesAsURL">
<img style="width: 100%;" height="400" src="{{image}}">
</span>
</div>
<div>
<input type="file" multiple (change)="onFileSelected($event)" style="display: none;" #fileUpload>
<button type="button" class="btn btn-secondary" (click)="fileUpload.click()">Wybierz zdjęcia</button>
</div>

Browser's console does lazy evaluation. Try doing:
console.log(JSON.stringify(images));
console.log(images[0]);
you will see it as an empty array.
In your case by the time you manually click onto the console log of "images", the file gets loaded and you see the content.
Since you need to read all the files and do a final emission, make all the file read events as an observable,
emit your array once all the Obsevables are complete.
public onFileSelected(event): void {
let loadenedObs = this._createFileReaderObs(event);
forkJoin(...loadenedObs).subscribe(() => {
// all the files are read, emit the array now.
this.images.emit(this.loadedImagesAsURL);
})
}
private _createFileReaderObs(event): [] {
let obsArr = [];
const filesList = event.target.files;
for (const file of filesList) {
const reader = new FileReader();
const loadenedEventObs = fromEvent(reader, 'loadend').pipe(
tap(() => {
this.loadedImagesAsURL.push(reader.result);
}),
take(1) // take one event to complete the Observable
);
obsArr.push(loadenedEventObs); // create an array of loadened observables.
reader.readAsDataURL(file);
}
return obsArr;
}

Related

HttpErrorResponse {headers: HttpHeaders, status: 415, statusText: "Unsupported Media Type"

Hi I tried to upload a csv file and convert it to json array and pass to the web api. But when I click the submit button I am getting this error. Anyone who can help to fix this?
Thank you :)
This is my .ts file in angular Here I tried to upload a csv file and convert it to json array.
changeListener(event: any) {
if (event.target.files.length > 0) {
const file = event.target.files[0];
this.myForm.patchValue({
fileSource: file
});
//File reader method
let reader: FileReader = new FileReader();
reader.readAsText(file);
reader.onload = (e) => {
let csv: any = reader.result;
let allTextLines = [];
allTextLines = csv.split(/\r|\n|\r/);
console.log('CSV: ', csv?.toString());
}
//JSON.stringify(file);
}
}
submit() {
const formData = new FormData();
formData.append('file', this.myForm.get('fileSource')?.value);
this.http.post('http://localhost:64233/api/employee', formData)
.subscribe(res => {
console.log(res);
alert('Upload Sussessful');
})
}
This is my .html file in angular
<form [formGroup]="myForm" (ngSubmit)="submit()">
<h1 style="text-align: center">File Upload</h1>
<br /><br />
<div class="form-group">
<label for="file">File</label>
<input class="form-control" formControlName="file" id="file" type="file" class="upload"
(change)="changeListener($event)" />
</div>
<button id="btnSave" class="btn btn-primary" type="submit">Submit</button>
</form>
This is the error I get when I click on submit button
I checked my web api in postman and it is working fine for json array. Really appreciate if you can help. Thank you
Ok I am talking to myself. But this is to help others.
Here in my .ts file, I have uploaded the file and read the data in the csv file as a string. But I have not converted it to json array correctly and push it to go to the web api.
So below is the working code. This may not be the perfect one but it works fine for me.
This is my .ts file in angular
export class FileUploadComponent implements OnInit {
myForm = new FormGroup({
file: new FormControl('', [Validators.required])
});
ngOnInit(): void {
this.resetForm();
}
constructor(private http: HttpClient, public fileUploadService: FileUploadService,
private toastr: ToastrService, private router: Router) { }
obj: any;
unique: any;
removeHeader: any;
// Maximum file size allowed to be uploaded = 1MB
maxSize: number = 1048576;
//upload file
fileUpload(event: any) {
if (event.target.files && event.target.files.length > 0) {
// Don't allow file sizes over 1MB
if (event.target.files[0].size < this.maxSize) {
const file = event.target.files[0];
console.log(file);
//File reader method
let reader: FileReader = new FileReader();
reader.readAsText(file);
reader.onload = (e) => {
let csv: any = reader.result;
let res: any[] = csv.split("\n");
//remove first element of the array
res.shift();
let jsonArray: any = [];
res.forEach(item => {
let singlePerson = item.split(",");
let singleObject = { employeeid: singlePerson[0], firstname: singlePerson[1], lastname: singlePerson[2], address: singlePerson[3] }
jsonArray.push(singleObject);
})
this.obj = jsonArray;
//check duplicates in csv file, remove, and return unique records
let unique = this.obj
.map((e: { [x: string]: any; }) => e['employeeid'])
.map((e: any, i: any, final: string | any[]) => final.indexOf(e) === i && i)
.filter((obje: string | number) => this.obj[obje])
.map((e: string | number) => this.obj[e]);
this.obj = unique;
}
}
else {
// Display error message
this.toastr.error("File is too large to upload");
}
}
}
resetForm() {
this.myForm.reset();
}
submit() {
this.fileUploadService.postFileUpload(this.obj);
this.resetForm();
}
}
This is my html file
<br /><br />
<form [formGroup]="myForm">
<h1 style="text-align: center">File Upload</h1>
<br /><br />
<div class="form-group">
<label for="file" style="font-size: 25px">File</label>
<input
class="form-control"
formControlName="file"
type="file"
accept=".csv"
class="upload"
(change)="fileUpload($event)"
/>
</div>
<div class="form-group">
<label> Please Upload a CSV or Text file of size less than 1MB </label>
</div>
<button class="btn btn-primary" type="submit" (click)="submit()">
Submit
</button>
</form>
This is my service class
export class FileUploadService {
messages: string[] = [];
constructor(private http: HttpClient, private toastr: ToastrService) { }
readonly baseURL = 'http://localhost:64233/api/employee';
myForm = new FormGroup({
file: new FormControl('', [Validators.required])
});
formData: FileUpload = new FileUpload();
//method for post request
postFileUpload(body: any) {
const requestOptions = { headers: new HttpHeaders({ 'content-type': "application/json" }) };
return this.http.post(this.baseURL, body, requestOptions)
.subscribe(
observer => {
this.toastr.success("File Uploaded Succesfully");
this.resetForm();
},
err => {
if (err.status == 500)
this.toastr.error("Empty File");
else
this.toastr.error("Please upload a file");
//console.log(err);
/* (error) => {
console.log(error); */
//throw new Error(error);
});
}
resetForm() {
this.myForm.reset();
this.formData = new FileUpload();
}
}
Here to display alerts I have used a toaster

Angular: ExpressionChangedAfterItHasBeenCheckedError when trying to disable button

I use mat-dialog to edit details of my profile page. I'm getting an ExpressionChangedAfterItHasBeenCheckedError when I click the 'Edit age' button and the dialog window pops up.
I decided to extract the styling of all edit dialogs into a single edit.component:
edit.component.html
<div class="navigation-control">
<mat-icon (click)="onCancelButtonClicked()"
class="close-button">close</mat-icon>
</div>
<div class="content-main">
<ng-content select=".content-main"></ng-content>
</div>
<div class="content-bot">
<button mat-raised-button
(click)="onCancelButtonClicked()">Cancel</button>
<button mat-raised-button
(click)="onActionButtonClicked()"
[lnDisableButton]="actionButtonDisabled">{{actionButtonValue}}</button>
</div>
edit.component.ts
#Component({ selector: 'ln-edit', ... })
export class EditComponent {
#Input() actionButtonValue: string;
#Input() actionButtonDisabled: boolean;
#Output() cancelButtonClicked = new EventEmitter<void>();
#Output() actionButtonClicked = new EventEmitter<void>();
onCancelButtonClicked() {
this.cancelButtonClicked.emit();
}
onActionButtonClicked() {
this.actionButtonClicked.emit();
}
}
To avoid the ExpressionChangedAfterItHasBeenCheckedError when trying to disable buttons and controls, I used this snippet. But that didn't solve this issue.
disable-button.directive.ts
#Directive({ selector: '[lnDisableButton]' })
export class DisableButtonDirective {
#Input('lnDisableButton') isDisabled = false;
#HostBinding('attr.disabled')
get disabled() { return this.isDisabled; }
}
The following is the contents of a mat-dialog window. This gets instantiated when I click the 'Edit age' button. When I remove the [actionButtonDisabled]="actionButtonDisabled", the error goes away, but obivously I need that line to make the functionality disable the button.
age-edit.component.html
<ln-edit [actionButtonValue]="actionButtonValue"
[actionButtonDisabled]="actionButtonDisabled"
(cancelButtonClicked)="onCancelButtonClicked()"
(actionButtonClicked)="onActionButtonClicked()">
<form [formGroup]="ageForm"
class="content-main">
<ln-datepicker formControlName="birthday"
[appearance]="'standard'"
[label]="'Birthday'"
class="form-field">
</ln-datepicker>
</form>
</ln-edit>
I handle the disabling/enabling the button in the 'ts' part of the mat-dialog popup.
age-edit.component.ts
#Component({ selector: 'ln-age-edit', ... })
export class AgeEditComponent implements OnInit, OnDestroy {
ageForm: FormGroup;
private initialFormValue: any;
actionButtonDisabled = true;
private unsubscribe = new Subject<void>();
constructor(
private editPhotoDialogRef: MatDialogRef<AgeEditComponent>,
private fb: FormBuilder,
#Inject(MAT_DIALOG_DATA) public dialogData: Date) { }
ngOnInit() {
this.initializeAgeForm();
this.loadDataToAgeForm(this.dialogData);
this.trackFormDistinct();
}
private initializeAgeForm(): void {
this.ageForm = this.fb.group({
birthday: null,
});
}
loadDataToAgeForm(birthday: Date | null): void {
if (!birthday) { return; }
this.ageForm.setValue({ birthday });
this.initialFormValue = this.ageForm.value;
}
get birthdayAC() { return this.ageForm.get('birthday') as AbstractControl; }
get actionButtonValue(): string {
return this.birthdayAC.value ? 'Update age' : 'Add age';
}
onCancelButtonClicked(): void {
this.editPhotoDialogRef.close();
}
onActionButtonClicked(): void {
this.editPhotoDialogRef.close({ ... });
}
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
ngOnDestroy() { ... }
}
I suspect this has something to do with content projection, but I'm not sure.
(...or perhaps with my custom 'ln-datepicker'?)
Any ideas?
Thanks.
From what I can tell, the problem resides in trackFormDistinct() method:
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
Looks like because of this.ageForm.valueChanges, will have different values in the 2 change detection cycles. I think this.ageForm.valueChanges emits due to <ln-datepicker>.
In a tree of form controls, if one node calls setValue, all its ancestors will have to be updated. I've written more about how Angular Forms work in this article.
I'm thinking of 2 alternatives:
skip the first emission of ageForm since it indicates the initialization of the form control tree, so this is irrelevant to the logic inside subscribe's callback.
this.ageForm.valueChanges.pipe(
skip(1),
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(/* .... */)
initialize actionButtonDisabled with false, since the error complains that it switched from true to false
actionButtonDisabled = false;

Is there any way to unsubscribe child component's Subscriptions when element is removing from DOM using `renderer2.removeChild()` in angular?

Rendering HelloComponent in AppComponent and when the element is removed from DOM by using renderer.removeChild(), HelloComponent's ngOnDestroy method is not firing. So unable to close the subscriptions of Hello Component.
Here is an example stackbliz
Wow, don't you want to destroy them with plain old *ngIf? It would make life so much easier. Anyway, you can use mutation observers.
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
Roughly, and crudely, it could look like this:
constructor(private ref: ElementRef) {}
ngOnInit() {
const ref = this.ref;
this.subscription = this.myObservable.subscribe(data => {
console.log(data);
});
const config = { childList: true };
const cb = function(mutationList, observer) {
for (const mutation of mutationList) {
for (const node of mutation.removedNodes) {
if(node === ref.nativeElement) {
console.log("I've just been destroyed");
}
}
}
};
const observer = new MutationObserver(cb);
observer.observe(this.ref.nativeElement.parentNode, config);
}
Stackblitz here:
https://stackblitz.com/edit/angular-wutq9j?file=src/app/hello.component.ts

Adding/removing HTML elements dynamically

I have created an angular 8 app that allows a user to choose a date from a angular material date picker control. The app then sends a request a Mongodb using NodeJS to return a list of available daily sessions. This part of my app is working fine. The returned daily sessions are dynamically displayed as buttons in the html. My code iterates through the list of available sessions and creates a button in the html using a angular *ngFor loop. My problem is that when the user then wants to choose another date from the date picker the new list of sessions is returned and the new list of available dates creates more buttons in the html using the *ngFor loop i.e. the new list of buttons is appended to the existing list of buttons. What I actually want is the previously displayed button in the HTML to be deleted from the DOM and the new list of buttons for the newly selected date to be displayed in their place. Does anybody know how to do this? Thanks!
Below is my component ts file:-
import { Component, OnInit, ViewChild, ElementRef, Renderer2 } from '#angular/core';
import { Tank } from '../../models/tank.model';
import { FormGroup, FormControl } from '#angular/forms';
import * as moment from 'moment';
import { TankService } from 'src/app/services/tank.service';
#Component({
selector: 'app-booking',
templateUrl: './booking.component.html',
styleUrls: ['./booking.component.css']
})
export class BookingComponent implements OnInit {
selectedDate: any;
convertedDate: string;
tankAvailability: Tank[];
dateSelectionButtonPressed = false;
tankOneAvailability: string[] = [];
bookingForm: FormGroup;
constructor(private tankService: TankService) { }
ngOnInit(): void {
this.bookingForm = new FormGroup({
enteredDate: new FormControl(),
});
}
onSelect(event) {
this.selectedDate = event;
this.convertedDate = moment(event).format('DD-MM-YYYY');
this.bookingForm.patchValue({
enteredDate: this.convertedDate
});
}
getTankAvailabilityByDate() {
this.dateSelectionButtonPressed = false;
this.tankAvailability = [];
if (this.convertedDate) {
this.tankService.getTankAvailabilityByDate(this.convertedDate)
.subscribe(tanks => {
this.tankAvailability = tanks.tanks;
this.populateAvailableSessions();
});
}
}
populateAvailableSessions() {
this.dateSelectionButtonPressed = true;
for(let i = 0; i < this.tankAvailability.length; i++) {
if (this.tankAvailability[i].tankNumber === 1) {
console.log(this.tankAvailability[i]);
if (this.tankAvailability[i].sessionOne === false) {
this.tankOneAvailability.push('07:00');
}
if (this.tankAvailability[i].sessionTwo === false) {
this.tankOneAvailability.push('09:00');
}
if (this.tankAvailability[i].sessionThree === false) {
this.tankOneAvailability.push('11:00');
}
if (this.tankAvailability[i].sessionFour === false) {
this.tankOneAvailability.push('13:00');
}
if (this.tankAvailability[i].sessionFive === false) {
this.tankOneAvailability.push('15:00');
}
console.log(this.tankOneAvailability);
}
}
}
}
Below is my component HTML file:-
<mat-card>
<div class="center-section">
<h1>Book a float</h1>
<p>
Select a date on the calendar below and we will display all the available float times
</p>
<form (submit)="getTankAvailabilityByDate()" [formGroup]="bookingForm">
<div class="calendar-wrapper">
<mat-calendar [selected]="selectedDate" (selectedChange)="onSelect($event)"></mat-calendar>
</div>
<mat-form-field class="invisible-field">
<input matInput formControlName="enteredDate">
</mat-form-field>
<button mat-raised-button color="primary" type="submit">Get availability</button>
</form>
</div>
</mat-card>
<mat-card *ngIf="dateSelectionButtonPressed">
<mat-card-header>
<mat-card-title>Tank 1</mat-card-title>
</mat-card-header>
<mat-card-content *ngFor="let session of tankOneAvailability">
<button mat-raised-button color="primary">{{session}}</button>
</mat-card-content>
</mat-card>
You would need to empty the array to show new list of buttons. Below code needs to be changed
populateAvailableSessions() {
this.dateSelectionButtonPressed = true;
// always create new array so new values are pushed
this.tankOneAvailability = [];
for(let i = 0; i < this.tankAvailability.length; i++) {
if (this.tankAvailability[i].tankNumber === 1) {
console.log(this.tankAvailability[i]);
if (this.tankAvailability[i].sessionOne === false) {
this.tankOneAvailability.push('07:00');
}
if (this.tankAvailability[i].sessionTwo === false) {
this.tankOneAvailability.push('09:00');
}
if (this.tankAvailability[i].sessionThree === false) {
this.tankOneAvailability.push('11:00');
}
if (this.tankAvailability[i].sessionFour === false) {
this.tankOneAvailability.push('13:00');
}
if (this.tankAvailability[i].sessionFive === false) {
this.tankOneAvailability.push('15:00');
}
console.log(this.tankOneAvailability);
}
}
}
Hope this helps.
You need to reinitialise the tankOneAvailability array each time you run a new query like you do for the tankAvailability (currently you keep pushing more values into it)
i.e.
getTankAvailabilityByDate() {
this.dateSelectionButtonPressed = false;
this.tankAvailability = [];
this.tankOneAvailability = []

How can I get the real image value from each item in my list and subscribe it to another list?

I have a list of services that have multiple property like serviceId, serviceName and photoProfile called from a database using a spring REST API.
The 'photoProfile' property only has the id of the profile picture which if you use the 'localhost:8082/downloadFile/'+photoProfile would get you the image which is in turn is stored in a folder in the spring project.
After looking for a while online, I've found how I can actually get the real image to display on my website but now I'm stuck since I need to do this for the whole list.
Here's my angular code:
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { LoginComponent } from '../login/login.component';
import { UserService } from '../user.service';
import { Observable, forkJoin } from 'rxjs';
import { HttpHeaders, HttpClient } from '#angular/common/http';
import { combineLatest } from 'rxjs/operators';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
loggedIn: boolean;
services: any[] = [];
imgSrc: any;
newList: any[] = [];
constructor(private router: Router, private service: UserService, private http: HttpClient) {
}
ngOnInit() {
this.service.getServices().subscribe(res => {
this.services = res;
console.log('services: ', this.services);
});
for (let i = 0; i < this.services.length; i++) {
const element = this.services[i];
this.getImage('http://localhost:4200/downloadFile/' + element.photoProfile).subscribe(data => {
this.createImageFromBlob(data);
});
this.newList.push(this.imgSrc);
console.log(this.newList);
//I want to add the element from the services list and the image value after being converted to the new list
}
}
getImage(imageUrl: string): Observable<Blob> {
return this.http.get(imageUrl, {responseType: 'blob'});
}
createImageFromBlob(image: Blob) {
const reader = new FileReader();
reader.addEventListener('load', () => {
this.imgSrc = reader.result;
}, false);
if (image) {
reader.readAsDataURL(image);
}
}
}
Thank you for your help.
You need to add the new list inside the ngOnInit after you are subscribing to the services list. Because currently. You don't have the services when the for loop runs. You need to run the for loop after you have the result from services. Like this:
ngOnInit() {
this.service.getServices().subscribe(res => {
this.services = res;
console.log('services: ', this.services);
for (let i = 0; i < this.services.length; i++) {
const element = this.services[i];
this.getImage('http://localhost:4200/downloadFile/' + element.photoProfile).subscribe(data => {
this.createImageFromBlob(data);
element.imgSrc = this.imgSrc;
this.newList.push(element);
});
console.log(this.newList);
}
}
});
I had similar situation, and method of Muhammad Kamran work particularry for me, because images loaded into array absolutely randomly. As i understand, the speed of FOR cycle is faster than picture download speed. The solution is - pushing into array in createImageFromBlob (in case of i'mgnome). In my case it was like this:
export interface File {
...
original_name: name of file;
linkToPicture: string;
image: any;
}
...
files: File[] = [];//array for incoming data with link to pictures
this.getTable(...)
...
getTable(sendingQueryForm: QueryForm){
this.queryFormService.getTable(sendingQueryForm)
.subscribe(
(data) => {
this.files=data as File[];
for (let i = 0; i < this.files.length; i++) {
this.getImage('/api/auth/getFileImage/' + this.files[i].linkToPicture).subscribe(blob => {
this.createImageFromBlob(blob,i);
});
}
},
error => console.log(error)
);
}
getImage(imageUrl: string): Observable<Blob> {
return this.http.get(imageUrl, {responseType: 'blob'});
}
createImageFromBlob(image: Blob, index:number) {
const reader = new FileReader();
reader.addEventListener('load', () => {
this.files[index].image = reader.result;
}, false);
if (image) {
reader.readAsDataURL(image);
}
}
and in HTML:
<div *ngFor="let block of files; let i = index" >
<mat-card class="grid-card">
<div>
<img [src]="block.image" width=120>
<p>{{block.original_name}}</p>
</div>
</mat-card>
</div>
I hope it will useful for someone and thanks for topic!