Angular 5 child emit 事件导致失去对子窗体的关注

Angular 5 child emit event causes loss of focus on child form

我有一个带有表单组和多个表单控件的子组件。我正在使用 valueChanges 函数来控制更改控件时发生的情况:

this.proposalItemForm.get('qtyControl').valueChanges.forEach(
    () => this.changeItem(this.proposalItemForm.get('qtyControl'))
);

更改项目时会发生以下情况:

changeItem(control) {
        // make sure the control is valid
        if (control.status=="VALID") {
            // first, make sure the data model matches the form model
            this.proposalItem = this.prepareSaveItem();
            // then submit the updated data model to the server
            this.proposalItemService.update(this.proposalItem).subscribe(
                data => {
                    this.proposalItem = data;
                },
                error => {
                    if (error.exception=="EntityNotFoundException") {
                        this.messagesService.error(error.message);
                    }
                    else {
                      this.messagesService.error("There was a problem updating the proposal item.");
                      console.error("Error: ", error);
                    }
                },
                () => { 
                    this.itemTotal = this.proposalItem.quantity*this.proposalItem.priceEach;
                    this.updateTotal.emit(this.proposalItem);
                }
            );
        }
    }

服务更新完全完成时出现问题。如果我删除 this.updateTotal.emit(this.proposalItem); 行,则遵循表单的 Tab 键顺序并且行为符合预期。触发的事件导致某些数字在父组件中重新计算。

(这是在设置提案,提案可能有多个项目。当一个项目的数量或价格发生变化时,它应该改变该项目的总计,然后是所有项目的总计。它是这个总计会在事件发出时更新。)

但是,当我离开行以发出事件时,焦点将完全移出表单,因此在表单中移动的用户必须单击返回表单才能继续。事实上,我可以看到有那么一小会儿,焦点转到选项卡索引中的下一个项目,但在更新完成后它就丢失了。

如何发出事件,但仍将焦点保持在表单上所属的位置?

EDIT/UPDATE: 我现在明白了为什么会发生这种情况,但我还不知道如何解决它。发生问题是因为 proposalItem 对象被传递给父对象。这是父级中发生的情况:

updateItem(item) {
    console.log(item);
    let itemIndex = this.proposalRevision.items.findIndex(it => it.proposalRevisionItemID == item.proposalRevisionItemID);
    this.proposalRevision.items[itemIndex] = item;
    this.updateItemTotal();
}

updateItemTotal() {
    this.grandTotal = this.proposalRevision.items.reduce((sum, item) => sum + (item.quantity*item.priceEach), 0);
}

updateItem 函数中,在项目数组中,父项更新为新项目。该项目通过以下方式绑定到父级中的模板:

<proposal-item *ngFor="let item of proposalRevision.items" ...></proposal-item>

所以当proposalRevision.items更新时,proposal-item(child)被刷新,因此失去了焦点。

处理此问题的最佳方法是不使用修改后的子项更新父项吗?如果我这样做,那么如果更新了父控件中的控件,它将带有父对象的旧版本的子控件发送到服务器。服务器仍然得到子版本的新版本,所以一切正常,但让不同版本的子版本四处闲逛似乎有点奇怪。此外,updateItemTotal 函数将不再正常工作,因为它会使用旧的子信息。我从根本上错过了什么吗?我是否必须一次提交整个子表单,然后焦点就无所谓了?

正确处理此问题的一种方法是拥有一个整体 FormGroup,然后在其下使用 FormArray 和更多 FormGroup。然后整个表单只有一个提交按钮,所以在填充每个控件后什么也不会发生。这种方法的美妙之处在于您不再需要发出输出事件,因为所有内容都是同一表单的一部分。对于我的情况,我有一份提案表。一份提案可以有一个或多个修订。每个 Revision 可以有一个或多个 Items。下面是我在提案组件中设置提案表单的方法:

constructor(
    private fb: FormBuilder, 
    private proposalService: ProposalService, 
    private proposalRevisionService: ProposalRevisionService,
    private messagesService: MessagesService, 
    public dialog: MatDialog) { 

    this.createForm();
}

// this just creates an empty form, and it is populated in ngOnChanges
createForm() {
    this.proposalForm = this.fb.group({
        IDControl: this.proposal.proposalID,
        propTimestampControl: this.proposal.proposalTimeStamp,
        notesControl: this.proposal.proposalNotes,
        estimatedOrderControl: this.proposal.estimatedOrderDate,
        nextContactControl: this.proposal.nextContactDate,
        statusControl: this.proposal.proposalStatus,
        revisionsControl: this.fb.array([])
    });
}

ngOnChanges() {
    this.proposalForm.reset({
        IDControl: this.proposal.proposalID,
        propTimestampControl: this.proposal.proposalTimeStamp,
        notesControl: this.proposal.proposalNotes,
        estimatedOrderControl: this.proposal.estimatedOrderDate,
        nextContactControl: this.proposal.nextContactDate,
        statusControl: this.proposal.proposalStatus
    });
    this.setProposalRevisions(this.proposal.revisions);
}

setProposalRevisions(revisions: ProposalRevision[]) {
    const revisionFormGroups = revisions.map(revision => this.fb.group({
        proposalRevisionIDControl: revision.proposalRevisionID,
        revisionIDControl: revision.revisionID,
        revDateControl: {value: moment(revision.revTimeStamp).format("D-MMM-Y"), disabled: true},
        subjectControl: [revision.subject, { updateOn: "blur", validators: [Validators.required] }],
        notesControl: [revision.notes, { updateOn: "blur" }],
        employeeControl: {value: revision.employee.initials, disabled: true},
        printTimeStampControl: [revision.printTimeStamp, { updateOn: "blur" }],
        deliveryContract: this.fb.group({
            deliveryTime: [revision.deliveryContract.deliveryTime, { updateOn: "blur", validators: [Validators.required, Validators.pattern("^[0-9]*")] }],
            deliveryUnit: [revision.deliveryContract.deliveryUnit, { validators: [Validators.required] }],
            deliveryClause: [revision.deliveryContract.deliveryClause, { validators: [Validators.required] }]
        }),
        shippingContract: this.fb.group({
            shippingTerms: revision.shippingContract.shippingTerms,
            shippingTermsText: [revision.shippingContract.shippingTermsText, { updateOn: "blur" }]
        }),
        paymentContract: this.fb.group({
            paymentTerms: [revision.paymentContract.paymentTerms, { updateOn: "blur" }],
            paymentValue: [revision.paymentContract.paymentValue, { updateOn: "blur" }],
            paymentUnit: revision.paymentContract.paymentUnit
        }),
        durationContract: this.fb.group({
            durationValue: [revision.durationContract.durationValue, { updateOn: "blur" }],
            durationUnit: revision.durationContract.durationUnit
        }),
        itemsControl: this.setProposalItems(revision.items)
    }));
    const revisionFormArray = this.fb.array(revisionFormGroups);
    this.proposalForm.setControl('revisionsControl', revisionFormArray);
}

setProposalItems(items: ProposalItem[]): FormArray {
    const itemFormGroups = items.map(item => this.fb.group({
        proposalRevisionItemIDControl: item.proposalRevisionItemID,
        itemIDControl: item.itemID,
        descriptionControl: [item.itemText, { validators: [Validators.required] }],
        qtyControl: [item.quantity, { validators: [Validators.required, Validators.pattern("^[0-9]*")] }],
        // The price can have any number of digits to the left of a decimal point, but a decimal point does not have to be present.
        // If a decimal point is present, then there must be 2-4 numbers after the decimal point.
        priceEachControl: [item.priceEach, { validators: [Validators.required, Validators.pattern("^[0-9]*(?:[.][0-9]{2,4})?$")] }],
        deliveryControl: this.fb.group({
            deliveryTypeControl: item.delivery.deliveryType,
            deliveryContractGroup: this.fb.group({
                deliveryTime: (item.delivery.deliveryContract==null ? 0 : item.delivery.deliveryContract.deliveryTime),
                deliveryUnit: (item.delivery.deliveryContract==null ? null : item.delivery.deliveryContract.deliveryUnit),
                deliveryClause: (item.delivery.deliveryContract==null ? null : item.delivery.deliveryContract.deliveryClause)
            })
        }),
        likelihoodControl: [item.likelihoodOfSale, { validators: [Validators.min(0), Validators.max(100)] }],
        mnfgTimeControl: item.mnfgTime
    }));
    return this.fb.array(itemFormGroups);
}

然后在提案组件模板中:

<form [formGroup]="proposalForm" autocomplete="off" novalidate>
    ...
    <div fxLayout="row" fxLayoutAlign="start center">
        <p class="h7">Revisions</p>
        <button mat-icon-button matTooltip="Add New Revision (The currently selected tab will be copied)" (click)="addRevision()"><i class="fa fa-2x fa-plus-circle"></i></button>
    </div>
    <mat-tab-group #revTabGroup class="tab-group" [(selectedIndex)]="selectedTab" formArrayName="revisionsControl">
        <mat-tab *ngFor="let rev of revisionsControl.controls; let last=last; let first=first; let i=index" [formGroupName]="i" [label]="rev.get('revisionIDControl').value">
            <ng-template mat-tab-label>
                <button mat-icon-button>{{rev.get('revisionIDControl').value}}</button>
                <button *ngIf="last && !first" mat-icon-button matTooltip="Delete Revision {{rev.get('revisionIDControl').value}}" (click)="deleteRevision(rev)"><i class="fa fa-trash"></i></button>
            </ng-template>
            <proposal-revision [proposalRevisionForm]="rev" [proposalMetadata]="proposalMetadata"></proposal-revision>
        </mat-tab>
    </mat-tab-group>
</form>

在 ProposalRevisionComponent 中,FormGroup 现在是输入而不是数据模型:

grandTotal: number = 0;

@Input()
proposalMetadata: ProposalMetadata;

@Input()
proposalRevisionForm: FormGroup;

constructor(private fb: FormBuilder, private proposalRevisionService: ProposalRevisionService, private proposalItemService: ProposalItemService,
            private messagesService: MessagesService, public dialog: MatDialog) { }

ngOnInit() {
    this.dataChangeListeners();
    //console.log(this.proposalRevisionForm);
}

dataChangeListeners() {
    this.proposalRevisionForm.get('itemsControl').valueChanges.forEach(() => 
        this.grandTotal = this.proposalRevisionForm.controls.itemsControl.controls.reduce((sum, control) => sum + (control.controls.qtyControl.value*control.controls.priceEachControl.value), 0)
    );
}

然后监听 Items 的变化并更新修订的总计变得非常简单。至于更新每个单独的项目总数,在 ProposalItemComponent 模板中,我只是使用:

<span>Total: {{(proposalItemForm.controls.qtyControl.value*proposalItemForm.controls.priceEachControl.value) | currency:'USD':'symbol':'1.2-2'}}</span>

这是可能的,因为 FormGroup 再次作为输入传递给 ProposalItemComponent。这是 ProposalRevisionComponent 模板的一部分:

<div [sortablejs]="proposalRevisionForm.controls.itemsControl" [sortablejsOptions]="sortEventOptions" formArrayName="itemsControl">
    <proposal-item *ngFor="let item of proposalRevisionForm.controls.itemsControl.controls; let i=index" [formGroupName]="i"
        [proposalItemForm]="item" 
        [proposalMetadata]="proposalMetadata">
    </proposal-item>
</div>

希望这些细节足以让遇到相同问题的人朝着正确的方向前进。