0

I am making a simple code to accept a few values in one Form and display the list of form values on the right side. I have created Modal Class of the Form Fields and on Submit, I am sending data to Service. In the other component i have subscribed to the data. For some unknown Reason, My functions in this second component are getting called Twice. Also I am accepting an image file or Image URL in form and generating a preview of the file selected in form and also in list. To Generate the preview of this file in Form, it works flawlessly but in Second component, the same code goes into infinite loop. Any

my main Form Component html

<div class="row form">
  <div class="col-8">
    <form [formGroup]="songMetadata" (submit)="onSubmit(songMetadata)">
      <div class="row">
        <div class="col form-data">
          <mat-form-field>
            <mat-label>Song Name</mat-label>
            <input matInput formControlName="songName" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Artist Name</mat-label>
            <input matInput formControlName="artistName" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Album Name</mat-label>
            <input matInput formControlName="albumName" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Spotify URL</mat-label>
            <input matInput formControlName="url" />
          </mat-form-field>
          <br />
          <br />
          <mat-form-field>
            <mat-label>Other Description</mat-label>
            <textarea
              matInput
              formControlName="description"
              rows="4"
            ></textarea>
          </mat-form-field>
          <br />
          <br />
          <button
            type="submit"
            class="btn btn-primary"
            [disabled]="
              songMetadata.invalid ||
              (!isImageFileSelected && !isImageURLEntered)
            "
          >
            Submit
          </button>
        </div>
        <div class="col image-upload">
          <div
            dropZone
            class="text-center dropzone"
            (hovered)="changeIsHover($event)"
            (dropped)="fileDropped($event)"
            [class.hovering]="isHovering"
          >
            <img
              *ngIf="isImageFileSelected || isImageURLEntered"
              [src]="imagePreview"
              alt=""
              width="192"
              height="190"
            />
            <div
              class="drop-text"
              *ngIf="!isImageFileSelected && !isImageURLEntered"
            >
              Drag And Drop File Here
            </div>
          </div>
          <input
            type="text"
            placeHolder="Or Enter URL"
            (blur)="loadPreviewFromURL($event)"
          />
          <br />
          <label for="files" class="btn btn-primary">Or Select Image</label>
          <br />
          <input
            id="files"
            style="visibility:hidden;"
            type="file"
            (change)="fileSelected($event)"
          />
          <br />
          <!-- <input type="file" /> -->
        </div>
      </div>
    </form>
  </div>
  <!-- List View -->
  <div class="col-4">
    <list></list>
  </div>
</div>

Main Component TS.

import { DataService } from "./data.service";
import { AngularFireStorageModule } from "@angular/fire/storage";
import { Component, OnInit } from "@angular/core";
import { FormGroup, FormBuilder, Validators } from "@angular/forms";
import { Observable } from "rxjs";
import { ISong } from "./song";
import { Data } from "@angular/router";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
  songMetadata: FormGroup;
  selectedFile: File = null;
  isImageFileSelected: boolean;
  isImageURLEntered: boolean;
  isHovering: boolean;
  song: ISong;
  imagePreview: any;
  // // Upload Related Stuff
  // task: AngularFireUploadTask;
  // snapshot: Observable<any>;

  constructor(
    private formBuilder: FormBuilder,
    private service: DataService // private storage: AngularFireStorage
  ) {
    this.isImageFileSelected = false;
    this.isHovering = false;
    this.isImageURLEntered = false;
  }

  ngOnInit() {
    this.songMetadata = this.formBuilder.group({
      songName: ["xzcvzxv", Validators.required],
      artistName: ["xzcvcxzv", Validators.required],
      albumName: ["zxcvcxv", Validators.required],
      url: ["zxcvvc", Validators.required],
      description: ["zxcvzcxv", Validators.required]
    });
  }

  fileSelected(event: any) {
    this.isImageFileSelected = true;
    this.isImageURLEntered = false;
    this.selectedFile = event.target.files[0];
    this.loadPreview();
  }

  fileDropped(event: FileList) {
    this.isImageFileSelected = true;
    this.isImageURLEntered = false;
    this.selectedFile = event.item(0);
    this.loadPreview();
  }

  onSubmit(songForm: FormGroup) {
    event.preventDefault();
    if (this.isImageFileSelected) {
      this.song = {
        name: songForm.value.songName,
        artist: songForm.value.artistName,
        album: songForm.value.albumName,
        url: songForm.value.url,
        description: songForm.value.description,
        imageFile: this.selectedFile,
        imageURL: null
      };
    } else {
      this.song = {
        name: songForm.value.songName,
        artist: songForm.value.artistName,
        album: songForm.value.albumName,
        url: songForm.value.url,
        description: songForm.value.description,
        imageFile: null,
        imageURL: this.imagePreview
      };
    }
    this.service.addSong(this.song);
  }

  changeIsHover(isHovering: boolean) {
    this.isHovering = isHovering;
  }

  loadPreview = () => {
    let reader = new FileReader();
    reader.readAsDataURL(this.selectedFile);
    reader.onload = event => {
      this.imagePreview = reader.result;
    };
    console.log("PREVIEW HIT");
  };

  loadPreviewFromURL(event: any) {
    console.log(event.target.value);
    this.isImageURLEntered = true;
    this.isImageFileSelected = false;
    this.imagePreview = event.target.value;
  }
}

list component HTML

<div *ngFor="let s of songs; let i = index">
  <div class="col-4">
    <img
      *ngIf="s?.imageURL"
      [src]="livePrvw(s?.imageURL)"
      width="100"
      height="100"
      alt="Image URL Preview"
    />
    <img
      *ngIf="s?.imageFile"
      [src]="livePreview(s?.imageFile)"
      width="100"
      height="100"
      alt="Image FILE Preview"
    />
  </div>
  <div class="col-8">
    {{ s.name }}
  </div>
</div>

listComponent TS

import { DataService } from "./../data.service";
import { ISong } from "./../song";
import { Component, OnInit, OnChanges } from "@angular/core";

@Component({
  selector: "list",
  templateUrl: "./list.component.html",
  styleUrls: ["./list.component.scss"]
})
export class ListComponent implements OnInit {
  songs: ISong[];
  // song: ISong;

  constructor(private service: DataService) {}

  ngOnInit() {
    this.service.songsAsObservable.subscribe(data => {
      this.songs = data;
    });
  }

  //  also getting called twice
  livePreview(file: File) {
    // Going into infinite loop

    // let newReader = new FileReader();
    // let imagePreview: any;
    // newReader.readAsDataURL(file);

    // newReader.onload = event => {
    //   imagePreview = newReader.result;
    // };

    console.log(file);
  }

  // getting called twice somehow
  livePrvw(imageURL: string) {
    console.log(imageURL);
    return imageURL;
  }
}

To Reiterate the methods in ListComponent Get Called Twice for no reason and FileReader Code working in Main Component does no work in ListComponent (Goes into infinite Loop)

Any Help Is Much Appreciated

9
  • I'd recommend not binding to functions that do any heavy processing. In your case I would create a view model and load all of the data up front. Then bind to the song view models Commented Mar 16, 2020 at 11:49
  • You're at the mercy of the change detection cycle at the moment Commented Mar 16, 2020 at 11:50
  • 1
    Btw, it's probably best not to store any sensitive info such as firebase keys in a public github repo. Commented Mar 16, 2020 at 12:11
  • Thanks for the Firebase Keys Blunder. But everything Else should Work. I tried putting ngOnChanges in list Component also to see how many times onChanges is fired but that is also fired once so as far as I know, there is a call to both functions twice whic I cannot Find. also Any Idea on the Infinite Loop FileReader in List Component?? Can you please run the code once in your macine and see if it behaves differently?? Commented Mar 16, 2020 at 12:40
  • I think your best bet here will be to recreate the problem with minimal code in stackblitz using an abstraction of what you currently have Commented Mar 16, 2020 at 12:53

1 Answer 1

0

Your problem

You have a form that allows users to upload an image or specify an image URL.

When the form is submitted, you render the uploaded image.

The functions that return the image URL either run multiple times or don't work.

My diagnosis

There are a few issues with your current solution.

  1. Multiple function execution

You are binding <img src /> to a function that resolves the URL. Every time change detection runs your function will be queried. You should do as little processing as possible in the change detection cycle.

In general this would be fixed by setting your component changeDetectionStrategy to OnPush, injecting a ChangeDetectorRef, and manually calling detectChanges() when you want to trigger change detection. However, I think this would be papering over the cracks in this case, so is not my suggested solution.

  1. Unable to resolve URL for uploaded file

You are attempting to bind <img src /> to a function that gets the data URL via FileReader. The file reader executes asynchronously, so you would need to wrap this in some kind of promise / observable and use the async pipe to call this.

BUT you shouldn't be doing this for the reason stated in point 1.

My approach

I'm going to focus on the specific problem of how to resolve the image URL, as you have a complex form that is mostly out of scope for this problem.

My main refactor here would be to take the URL resolution out of the change detection cycle. I would resolve the URL first, and then add the item to the array with a resolved URL.

My implementation

I am going to abstract your problem into a simple app where you can upload an image and/or specify an image URL. The resolved images will be displayed after the form is submitted.

The component is fairly trivial, so I will only show the service here:

image.service.ts

export class ImageService {
  private images$ = new BehaviorSubject<{ url: string, type: string }[]>([]);
  private images: { url: string, type: string }[] = [];

  addFromFile(file: File) {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = event => {
      this.addImage({
        url: reader.result.toString(),
        type: 'fromFile'
      });
    };    
  }

  addFromUrl(url: string) {
    this.addImage({
      url: url,
      type: 'fromUrl'
    });
  }

  getImages(): Observable<{ url: string, type: string }[]> {
    return this.images$.asObservable();
  }

  private addImage(image: { url: string, type: string }) {
    this.images.push(image);
    this.images$.next(this.images);
  }
}

Notice how the images are only pushed into the array and subject once the URL has been resolved.

I've definitely simplified this a lot compared to your form, but I think you could take some inspiration from this approach to fix your problem.

DEMO: https://stackblitz.com/edit/angular-yhnnwv

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.