1

I have a property called files in my child component, which is an input type=file. With that property I can check if the user selected a file to upload so I can disable the submit button if there are no files inside the input. My problem is that now I have the submit button in the parent component so I can't check the property files to enable the button when there's a file inside the input.

So, how can I access the files property from the parent component and check if the input contains a file or not?

I already tried with @ViewChild decorator with something like this this.childComponent.files but I'm getting undefined. I also tried @Output but I could not make it work.

EDIT: ADDED CODE

Parent Component TS:

import { Component, OnInit, ViewChild } from '@angular/core';
import { StepState } from '@covalent/core';
import { StepThreeComponent } from './step-three/step-three.component';
import { StepTwoComponent } from './step-two/step-two.component';
import { StepOneComponent } from './step-one/step-one.component';

@Component({
  selector: 'app-user-verification',
  templateUrl: './user-verification.component.html',
  styleUrls: ['./user-verification.component.scss']
})
export class UserVerificationComponent implements OnInit {

  @ViewChild(StepThreeComponent) stepThreeComponent: StepThreeComponent;
  @ViewChild(StepTwoComponent) stepTwoComponent: StepTwoComponent;
  @ViewChild(StepOneComponent) stepOneComponent: StepOneComponent;

  checkFiles: any;

  //EXECUTE ALL UPLOAD FUNCTIONS

  uploadAllFiles() {
    this.stepThreeComponent.uploadSingle();
    this.stepOneComponent.uploadSingle();
    this.stepTwoComponent.uploadSingle();
    console.log('No FUnciona')
  }

  ngOnInit() {
    this.checkFiles = this.stepOneComponent.files;
    console.log(this.checkFiles)
  }

}

Parent Component HTML:

<td-steps>
  <td-step #step1 sublabel="Suba una copia de la parte delantera de su Cédula de Identidad" [active]="true" [disabled]="false" (activated)="activeStep1Event()" [state]="stateStep1" (deactivated)="deactiveStep1Event()">
      <ng-template #head1 td-step-label><span>Foto de Cédula de Identidad (I)</span></ng-template>
      <app-step-one></app-step-one>
  </td-step>
  <td-step #step2 [active]="true" label="Foto de Cédula de Identidad (II)" sublabel="Suba una copia de la parte trasera de su Cédula de Identidad" [state]="stateStep2" [disabled]="false" disableRipple> 
      <app-step-two></app-step-two>
  </td-step>
  <td-step #step3 label="Selfie con Cédula de Identidad" sublabel="Subir Selfie" [state]="stateStep3" [disabled]="true" [active]="true">
      <app-step-three></app-step-three>
      <ng-template td-step-actions>
          <button [disabled]="!checkFiles" (click)="uploadAllFiles()">SUBIR TODO</button>
      </ng-template>
  </td-step>
</td-steps>

children component TS:

import { Component } from '@angular/core';
import { UploadService } from '../../../services/upload.service';
import * as firebase from 'firebase/app';
import "firebase/storage";
import { Upload } from '../../../services/upload';
import * as _ from "lodash";

@Component({
  selector: 'app-step-one',
  templateUrl: './step-one.component.html',
  styleUrls: ['./step-one.component.scss']
})
export class StepOneComponent {

  selectedFiles: FileList;
  currentUpload: Upload;
  files: any;
  disabled: any;

  constructor(private upSvc: UploadService) { }

    //Upload Service
    detectFiles(event) {
      this.selectedFiles = event.target.files;
  }

  uploadSingle() {
    let uid = firebase.auth().currentUser.uid;
    let file = this.files
    this.currentUpload = new Upload(file);
    this.upSvc.pushUpload(this.currentUpload, uid)
  }

}

children Component HTML:

<div>
    <div layout="row">
        <md-input-container tdFileDrop [disabled]="disabled" (fileDrop)="files = $event" (click)="fileInput.inputElement.click()" (keyup.enter)="fileInput.inputElement.click()" (keyup.delete)="fileInput.clear()" (keyup.backspace)="fileInput.clear()" flex>
            <input mdInput placeholder="select or drop files" [value]="files?.length ? (files?.length + ' files') : files?.name" [disabled]="disabled" readonly/>
        </md-input-container>
        <button md-icon-button *ngIf="files" (click)="fileInput.clear()" (keyup.enter)="fileInput.clear()">
            <md-icon>cancel</md-icon>
        </button>
        <td-file-input class="push-left-sm push-right-sm" #fileInput [(ngModel)]="files" multiple [disabled]="disabled">
            <md-icon>folder</md-icon>
            <span class="text-upper">Browse...</span>
        </td-file-input>
    </div>

    <div *ngIf="currentUpload">
        <div class="progress">
            <div class="progress-bar progress-bar-animated" [ngStyle]="{ 'width': currentUpload?.progress + '%' }"></div>
        </div>
        Progress: {{currentUpload?.name}} | {{currentUpload?.progress}}% Complete
    </div>
</div>

EDIT #2:

html parent:

<app-step-one #StepOneComponent></app-step-one>

ts parent:

  ngAfterViewInit() {
    this.checkFiles = this.stepOneComponent.getFiles();
  }

  getFiles() {
   this.checkFiles = this.stepOneComponent.getFiles();
    console.log(this.checkFiles);
  }

ts child added:

  getFiles() {
    return this.files
    }

I also tried this.stepOneComponent.files; but it was not working either. After that I created getFiles() in child component to see if I could get the file inside the input and it worked. I was able to do a console.log() from the parent component and get the file data. But what I need to do is to check for changes automatically and not with a function inside a button.

Edit #3 (for duplicate): That question doesn't help me because I need to check for changes automatically and not manually.

claudiomatiasrg
  • 578
  • 4
  • 11
  • 31

1 Answers1

1

Give your child class some public elements like isValid and maybe some $valid: BehaviorSubject<boolean> that can be consumed via @ViewChild inside of your parent component.

@Component({ 
    selector: 'parent-component',
    template: '<child-component #childComponent></child-component>'
})
export class ParentComponent implements AfterViewInit {
    @ViewChild('childComponent') childComponent: ChildComponent;

    ngAfterViewInit(): void {
        // do stuff here otherwise your @ViewChildren might not be available
        this.childComponent.$valid.subscribe(isValid => {
             // do validity related things
        })
    }
}

EDIT: After seeing your code, I believe your @ViewChild implementation is incorrect:

@ViewChild(StepOneComponent) stepOneComponent: StepOneComponent;

Doesn't do anything. The decorator cannot find anything in your parent component markup decorated with #StepOneComponent.

In your markup you need:

<app-step-one #StepOneComponent></app-step-one>

And in your class:

@ViewChild('StepOneComponent') stepOneComponent: StepOneComponent;

EDIT part more:

@ViewChild('thisIsADecorator') someVariableName: SomeComponentClass;

Is a pointer to:

<child-component #thisIsADecorator></child-component>

Which is an instance of a SomeComponentClass with a selector of child-component. The reason you need to use AfterViewInit is that the child component won't be available; from Angular's docs:

ngAfterViewInit() Respond after Angular initializes the component's views and child views.

In your parent class the @ViewChild(StepOneComponent) stepOneComponent: StepOneComponent; doesn't do anything.

Your parent markup:

<td-steps>
  <td-step #step1 sublabel="Suba una copia de la parte delantera de su Cédula de Identidad" [active]="true" [disabled]="false" (activated)="activeStep1Event()" [state]="stateStep1" (deactivated)="deactiveStep1Event()">
      <ng-template #head1 td-step-label><span>Foto de Cédula de Identidad (I)</span></ng-template>
      <app-step-one></app-step-one>// <---- This is the view child you're trying to get a handle on, but there is nothing for Angular to use to find it
  </td-step>

If you fix those things then what you're doing will work.


EDIT for exposing property getter

The trick here is that you have bound your td-file-input with ngModel pointing to a property files in StepOneComponent. In order to set yourself up with a change listener all you need to do is take advantage of a getter/setter of files.

private _files: any;
private $_files: BehaviorSubject<any> = new BehaviorSubject();

public get files(): any {
    return this._files;
}

public set files(val: any) {
    this.$_files.next(this._files = val);
}

public get $files(): Observable<any> {
    return this.$_files.asObservable();
}

Then in your parent component:

ngAfterViewInit() { // <-- I would really switch to this from ngOnInit
    this.stepOneComponent.$files.subscribe((newVal: any) => {
        console.log(newVal);
        // do stuff
    });
}
Mark Cooper
  • 406
  • 3
  • 13
  • Thanks for your reply! I didn't understand exactly what I had to do. Should I keep my code with the markup or should I do what you said first? – claudiomatiasrg Nov 02 '17 at 23:23
  • Let's see if I can break this down a bit... In order for the parent component to be able to access the actual instance of the child class that you've added in your markup, the parent needs a pointer to the child, hence `@ViewChild('StepOneComponent') stepOneComponent: StepOneComponent`. ViewChild actually looks in the markup for the decorator passed to it, in this case `#StepOneComponent`. This could be `@ViewChild('#bob') stepOneComponent: StepOneComponent` as long as you decorate the markup correctly `` – Mark Cooper Nov 03 '17 at 02:17
  • It's not detecting any changes in `files` properties. I tried to console log when I enter some file in the input and check if it worked with a button but it keeps returning undefined – claudiomatiasrg Nov 03 '17 at 14:43
  • @ClaudioRamírezGuichard can you update your code above to show what you have at this point? We can go into change emitters once I can see that you have a pointer at the correct component. – Mark Cooper Nov 03 '17 at 14:53
  • I just updated the code Mark, thanks for your help and patience! – claudiomatiasrg Nov 03 '17 at 15:32
  • @ClaudioRamírezGuichard I've added to my answer. You can take that pattern many places. For instance instead of reading the property you could just publish validity changes (or both). – Mark Cooper Nov 04 '17 at 03:31
  • Exactly in this part of the code `new BehaviorSubject();` Im getting an error that states that Expected 1 argument but got 0 – claudiomatiasrg Nov 05 '17 at 18:24
  • Had to pass `this.files` to the function. It worked @MarkCooper Thank you for your time and patience again! – claudiomatiasrg Nov 05 '17 at 18:34