嵌套一个组件,需要从该组件获取一个属性值给父级并设置另一个
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 个文件加载到上传容器中,他们可以选择为每个文件组件独立添加一些描述文本,我只是将其设置为文件组件内的字符串属性和双向绑定使用 ngModel
从 html 中获取它。在父组件上,有一个按钮将启动上传过程。我正在尝试遍历已推送到数组的文件组件实例,并且无法从循环内的父级访问 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
应该负责上传每个文件,这样你就可以更好地控制从哪里调用它的每个文件。以上是关于嵌套一个组件,需要从该组件获取一个属性值给父级并设置另一个的主要内容,如果未能解决你的问题,请参考以下文章
第十讲、Vue3.x父组件给子组件传值、Props、Props验证、单向数据流