嵌套一个组件,需要从该组件获取一个属性值给父级并设置另一个

Posted

技术标签:

【中文标题】嵌套一个组件,需要从该组件获取一个属性值给父级并设置另一个【英文标题】:nesting a component and need to get a property value to parent from said component and set another 【发布时间】:2019-10-30 04:30:28 【问题描述】:

我有一个文件上传组件,我正在根据用户的文件列表选择在其中启动多个文件组件实例。

一旦用户将 n 个文件加载到上传容器中,他们可以选择为每个文件组件独立添加一些描述文本,我只是将其设置为文件组件内的字符串属性和双向绑定使用 ngModelhtml 中获取它。在父组件上,有一个按钮将启动上传过程。我正在尝试遍历已推送到数组的文件组件实例,并且无法从循环内的父级访问 description 属性。

这是问题一。问题二是我还想在子组件(isUploading)上设置一个布尔属性,以便我可以提供一些用户反馈,表明该特定文件正在进行中。现在我只是想专门为那个子文件组件显示一个ProgressSpinner。但它不是基于我在父组件的循环内更新引用而自动更新的。

我确定这是我在事件或类似事件中缺少的东西,但我正在努力将它们组合在一起,找不到适合我的场景的好资源。

这里是父(文件上传组件)ts:

import  Component, OnInit, Input  from '@angular/core';
import  MatFileComponent  from './mat-file.component';

@Component(
  selector: 'mat-file-upload',
  templateUrl: './mat-file-upload.component.html',
  styleUrls: ['./mat-file-upload.component.css']
)
export class MatFileUploadComponent implements OnInit 
  constructor()  
  fileList: MatFileComponent[]
  @Input() apiEndpoint: string;
  @Input() parentContainerId: string;
  hasFiles: boolean;
  bucketDescription: string;
  addFilesToList(files: File[]): void 
    this.fileList = [];
    for (let file of files) 
      // generate the file component here in code then bind them in the loop in the HTML
      let fileComponent = new MatFileComponent()
      fileComponent.fileData = file;
      fileComponent.fileDescription = '';
      fileComponent.fileName = file.name;
      fileComponent.fileType = file.type;
      this.fileList.push(fileComponent);
    
    this.hasFiles = true;
  
  startUpload(): void 
    if (!this.fileList || !this.fileList.length || !this.hasFiles) 
      return;
    
    for (let fileComponent of this.fileList) 
      console.log("desc: " + fileComponent.fileDescription);
      fileComponent.isUploading = true;
    
   
  ngOnInit() 
  

这是它的支持 HTML:

<input type="file" hidden name="addToList" [class]="ng-hide" #file multiple id="addToList" (change)="addFilesToList(file.files)" />
<label for="addToList" class="mat-raised-button">
  Select Files To Upload
</label>
<div *ngIf="fileList && fileList.length">
  <mat-file *ngFor="let file of fileList"
            [fileName]="file.fileName"
            [fileData]="file.fileData"
            [fileType]="file.fileType"
            [projectId]="projectId"></mat-file>
</div>
<mat-card class="card-footer" *ngIf="hasFiles">
  <mat-form-field>
    <textarea matInput required placeholder="*Required* file bucket description..." [(ngModel)]="bucketDescription"></textarea>
  </mat-form-field>
  <button class="mat-raised-button submit-form" (click)="startUpload()" [disabled]="!bucketDescription || !bucketDescription.length > 0">
    Upload Files
  </button>
</mat-card>

这里是子(文件组件)ts:

import  Component, OnInit, Input  from '@angular/core';
import  IFile  from '../Interfaces/IFile';

@Component(
  selector: 'mat-file',
  templateUrl: './mat-file.component.html',
  styleUrls: ['./mat-file.component.css']
)
export class MatFileComponent implements OnInit, IFile 
  @Input() fileName: string;
  @Input() fileData: File;
  @Input() fileType: string;
  @Input() projectId: number;
  public isUploading: boolean;
  fileDescription: string;

  imageLocalUrl: any;
  componentLoaded: boolean = false

  constructor()  
  get isFileImage(): boolean 
    return this.fileType.toLowerCase().indexOf('image') > -1;
  

  ngOnInit() 
    var reader = new FileReader();
    reader.readAsDataURL(this.fileData);
    reader.onload = (event) => 
      this.imageLocalUrl = reader.result;
    
    this.componentLoaded = true;
  

及其支持的 HTML:

<div *ngIf="componentLoaded" class="file-card">
  <mat-card class="mat-card-image">
    <mat-card-subtitle> fileName </mat-card-subtitle>
    <mat-card-content>
      <div *ngIf="imageLocalUrl && isFileImage" class="image-thumb file-thumb" [style.backgroundImage]="'url(' +imageLocalUrl+ ')'"></div>
      <mat-form-field>
        <textarea matInput placeholder="*Optional* file description..." [(ngModel)]="fileDescription"></textarea>
      </mat-form-field>
    </mat-card-content>
    <div *ngIf="(isUploading)" class="loading-indicator-shade">
      <mat-progress-spinner class="loading-indicator" mode="indeterminate"></mat-progress-spinner>
    </div>
  </mat-card>
</div>

这就是我使用标签指令启动文件上传组件的方式:

<mat-file-upload [apiEndpoint]="'/api/ProjectApi/UploadFile'" [parentContainerId]="projectId"></mat-file-upload>

我为所有这些代码呕吐表示歉意,但我想画一个非常清晰的画面。我确信这两个项目都很简单,但我很难过。

TIA

【问题讨论】:

【参考方案1】:

将@Output() 添加到您的子组件,以便它可以将更改发送回父组件。

export class MatFileComponent implements OnInit, IFile 
   @Output() uploadComplete = new EventEmitter<boolean>();

   ...

   onComplete() 
      this.uploadComplete.next(true);
   

   ... 

现在在父组件中监听这个事件。

<div *ngIf="fileList && fileList.length">
  <mat-file *ngFor="let file of fileList"
            [fileName]="file.fileName"
            [fileData]="file.fileData"
            [fileType]="file.fileType"
            [projectId]="projectId">
            (onComplete)="handleCompleteEvent($event)"</mat-file>
</div>

并在父组件中引入上述方法来处理来自子组件的发出事件。

.ts

handleCompleteEvent(status: boolean) 
   if (status) 
      // do something here...
   

【讨论】:

谢谢你,但我不确定这是我要找的。父组件将负责上传,因此它需要在开始上传(并完成)时通知子组件。这在我看来就像你已经得到它,所以孩子正在向父母发送通知。顺便说一句,我能够使用这种方法来解决我的 OP 中的另一个问题。如何从父组件更改子组件的属性,以便 *ngIf 将根据父组件的操作显示或隐藏? 可以在子组件中添加@Input(),然后从父组件传入这个值。 正如我在另一篇文章中提到的,你昨天帮助了我,我对 Angular 还是很陌生,今天早上以全新的心态快速审查代码后,我意识到我哪里出错了。我正在引用我在代码中初始化的文件组件的不同实例。你昨天帮我做的是用指令在 HTML 中重新初始化它们。所以这导致了一个全新的,但仍然是主题问题。有没有办法引用从文件上传组件的 HTML 片段中的代码文件中的*ngFor 旋转起来的文件组件实例的集合? 据我所知 - 不(至少不是在 html 中)。可能您需要更好地重组您的组件?上面的答案使您可以从孩子->父母那里接收数据(我认为这回答了您问题的一部分?)不确定这是否是您要问的,但是如果您想在父母中获得孩子的参考,您可以使用 @ViewChild 装饰器。 在我做了一个大的重构之后,直到现在我才看到你的消息。使用您的 EventEmitter 方法,我得到了一点创意。我所拥有的完美。我将在我的解决方案中发布答案,因为要在此评论中输入很多内容。我不确定它是否是最好的模式,但它对我的场景非常有效。我很想看看你是否认为我忽略了它的任何重大缺陷。【参考方案2】:

根据 Nicholas 的回答,我想出了这个解决方案。我在初始化的ngOnInit() 中触发EventEmitter 并将this 传递回父文件上传组件。在该组件的类中,我只是将对子组件的引用添加到一个数组中,这使我可以正确访问它的成员。

文件上传组件:

import  Component, OnInit, Input  from '@angular/core';
import  MatFileComponent  from './mat-file.component';
import  FileUploadService  from '../Services/file-upload.service';
import  catchError, map  from 'rxjs/operators';
import  IFile  from '../Interfaces/IFile';

@Component(
  selector: 'mat-file-upload',
  templateUrl: './mat-file-upload.component.html',
  styleUrls: ['./mat-file-upload.component.css']
)
export class MatFileUploadComponent implements OnInit 
  constructor(private fileUploadService: FileUploadService)  
  matFileComponents: MatFileComponent[] = [];
  fileList: any[];
  @Input() apiEndpoint: string;
  @Input() parentContainerId: string;
  hasFiles: boolean;
  bucketDescription: string;
  bucketId: string = "0";
  uploaded: number = 0;
  errorMessage: string;
  addFilesToList(files: File[]): void 
    this.fileList = [];
    for (let file of files) 
      // generate the file collectoin here then loop over it to create the children in the HTML
      let fileComponent = 
        
          "fileData": file,
          "fileName": file.name,
          "fileType": file.type
        
      this.fileList.push(fileComponent);
    
    this.hasFiles = true;
  
  addInstanceToCollection(matFileComponent: MatFileComponent): void 
    this.matFileComponents.push(matFileComponent);
  
  startUpload(): void 
    for (let matFileComponent of this.matFileComponents) 
      // just for testing, make sure the isUploading works and log the description to the console to make sure it's here
      // hell, let's just log the whole instance ;)  The file upload service will be called from here
      matFileComponent.isUploading = true;
      console.log(matFileComponent.fileDescription);
      console.log(matFileComponent);
    
  

  ngOnInit() 
  

它支持的 HTML:

<input type="file" hidden name="addToList" [class]="ng-hide" #file multiple id="addToList" (change)="addFilesToList(file.files)" />
<label for="addToList" class="mat-raised-button">
  Select Files To Upload
</label>
<div *ngIf="fileList && fileList.length" >
  <mat-file *ngFor="let file of fileList"
            [fileName]="file.fileName"
            [fileData]="file.fileData"
            [fileType]="file.fileType"
            [projectId]="projectId"
            (instanceCreatedEmitter)="addInstanceToCollection($event)"></mat-file>
</div>
<mat-card class="card-footer" *ngIf="hasFiles">
  <mat-form-field>
    <textarea matInput required placeholder="*Required* file bucket description..." [(ngModel)]="bucketDescription"></textarea>
  </mat-form-field>
  <button class="mat-raised-button submit-form" (click)="startUpload()" [disabled]="!bucketDescription || !bucketDescription.length > 0">
    Upload Files
  </button>
</mat-card>

文件组件:

import  Component, OnInit, Input, EventEmitter, Output  from '@angular/core';
import  IFile  from '../Interfaces/IFile';

@Component(
  selector: 'mat-file',
  templateUrl: './mat-file.component.html',
  styleUrls: ['./mat-file.component.css']
)
export class MatFileComponent implements OnInit, IFile 
  @Input() fileName: string;
  @Input() fileData: File;
  @Input() fileType: string;
  @Input() projectId: number;
  @Input() isUploading: boolean = false;
  @Output() instanceCreatedEmitter = new EventEmitter<MatFileComponent>();
  public fileDescription: string;

  imageLocalUrl: any;
  componentLoaded: boolean = false

  constructor()  
  get isFileImage(): boolean 
    return this.fileType.toLowerCase().indexOf('image') > -1;
  

  ngOnInit() 
    var reader = new FileReader();
    reader.readAsDataURL(this.fileData);
    reader.onload = (event) => 
      this.imageLocalUrl = reader.result;
    
    this.componentLoaded = true;
    // now emit this instance back to the parent so they can add it to their collection
    this.instanceCreatedEmitter.emit(this);
  

它支持的 HTML:

<div *ngIf="componentLoaded" class="file-card">
  <mat-card class="mat-card-image">
    <mat-card-subtitle> fileName </mat-card-subtitle>
    <mat-card-content>
      <div *ngIf="imageLocalUrl && isFileImage" class="image-thumb file-thumb" [style.backgroundImage]="'url(' +imageLocalUrl+ ')'"></div>
      <mat-form-field>
        <textarea matInput placeholder="*Optional* file description..." [(ngModel)]="fileDescription"></textarea>
      </mat-form-field>
    </mat-card-content>
    <div *ngIf="isUploading" class="loading-indicator-shade">
      <mat-progress-spinner class="loading-indicator" mode="indeterminate"></mat-progress-spinner>
    </div>
  </mat-card>
</div>

这解决了我在 OP 中的两个问题,因为现在我引用了需要挂钩其属性的实际组件。

我也读过 Nicholas 的 @ViewChild 选项,但我已经写了这个,除非你告诉我这个解决方案出于某种原因很糟糕,否则我会接受“如果它没有坏……”这里的路。

【讨论】:

IMO,问题在于现在您将eventEmitter 绑定到MatFileComponent 的所有实例,这不被认为是最佳做法。也许,您需要重新审视组件的设计。 MatFileUploadComponent 应该只负责上传一个文件。然后你可以管理另一个组件中的文件列表,比如MatFileListUploadComponent?现在可能从此组件中,您可以循环并在循环中调用其他MatFileComponent。或者甚至更好地将整个上传功能委托给服务。 总之,可以认为是主观的 //有意见的等。为我的烦恼点赞? 5, 1 的答案怎么样,4 的 cmets ;) 再次感谢。 @NicholasK 很好奇为什么你认为MatFileUploadComponent 组件应该只负责一个文件?该层在MatFileComponent 被分解(单个文件)。 MatFileUploadComponent 的目的是能够处理完整的集合。它当然可以只处理一个文件,但它现在的架构方式已经能够处理一个或多个文件。我有点迷失为什么你认为我需要为列表部分添加第三个组件...... 哈哈,谢谢(不相关 - 只是对答案的支持增加了代表)。 Angular 组件应该尽可能轻。因此,所有繁重的工作都应该委托给服务。 IMO,我觉得 MatFileComponent 应该负责上传每个文件,这样你就可以更好地控制从哪里调用它的每个文件。

以上是关于嵌套一个组件,需要从该组件获取一个属性值给父级并设置另一个的主要内容,如果未能解决你的问题,请参考以下文章

iframe如何传值给父iframe

React 子组件给父组件传值、整个组件、方法

第十讲、Vue3.x父组件给子组件传值、Props、Props验证、单向数据流

vue父组件异步获取数据传值给子组件

Vue3 依赖注入------父级组件与子孙级组件之间的数据传递

不触发事件,vue子组件传值给父组件