Angular如何在表单提交时获取按钮元素

4
我有一个页面上有多个表单,每个表单上有许多按钮。我想在按钮被按下后实现加载旋转器。当我使用普通的点击事件时,可以传递按钮:
HTML
<button #cancelButton class="button-with-icon" type="button" *ngIf="hasCancel" mat-raised-button color="warn" [disabled]="isLoading" (click)="clickEvent(cancelButton)">
    <mat-spinner *ngIf="isLoading" style="display: inline-block;" diameter="24"></mat-spinner>
    Cancel
</button>

TS

clickEvent(button: MatButton) {
    console.log(button);
}

在这种情况下,按钮元素会被传递并且您可以访问它以添加一个 loading 类到该按钮。
然而,如果您尝试使用提交按钮做同样的事情,它将被视为 undefined:
HTML
<form (ngSubmit)="save(saveButton)">
    <button #saveButton class="button-with-icon" type="submit" *ngIf="hasSave" mat-raised-button color="primary" [disabled]="isLoading">
        <mat-spinner *ngIf="isLoading" style="display: inline-block;" diameter="24"></mat-spinner>
        Save
    </button>
</form>

TS

save(button: MatButton) {
    console.log(button);
}

在这种情况下,按钮是undefined,因为按钮上的*ngIf将其从表单的范围中屏蔽掉了。我可以删除*ngIf并隐藏按钮,但这会使按钮留在DOM中,我不想这样做。
以下是一个Stack Blitz示例:https://stackblitz.com/edit/angular-zh7jcw-mrqaok?file=app%2Fform-field-overview-example.html 我尝试向保存按钮添加另一个单击事件来设置按钮为加载状态,但是单击事件先触发,并在阻止提交事件被调用之前将按钮设置为禁用状态。
我已查看提交事件,但没有找到任何与所点击的按钮相关的内容。
有什么建议吗?

首先...为什么需要像这样操作DOM(添加加载)?有什么特殊的原因吗? - AT82
保存和取消功能将异步调用 web 服务器。我想要在异步调用正在处理时禁用按钮并在按钮上显示旋转图标。我正在制作一个动态表单组件。该组件可以有多个表单和每个表单上的多个按钮。因此,通过固定变量跟踪哪个按钮正在加载似乎不如仅向按钮的 DOM 添加 "loading" 类效率高。 - Curtis
好的,我明白了。有些人只是在布尔标志足够的情况下这样做,但我在这种情况下理解你的原因。 :) - AT82
@Curtis 我不确定你在这里关注的效率是什么。重点是Angular通过将状态存储在组件中而不是DOM中来实现此目的。这样做的代码更少,而且性能并没有降低。 - Ruan Mendes
5个回答

3
更新 我现在理解了你的具体问题。问题出在按钮上的*ngIf。它破坏了引用。将其更改为[hidden],就可以解决问题。这是一个更新后的 StackBlitz:https://stackblitz.com/edit/angular-zh7jcw-pcuuyv 关于为什么会发生这种情况,我认为这篇文章描述得很清楚:https://dev59.com/r1oV5IYBdhLWcg3wIb74#36651625 原始回答:
我甚至不需要传递它。只需在您的类中添加以下内容即可:
 @ViewChild('saveButton') button;

然后你可以在保存方法中引用它:

save() {
    console.log(this.saveButton);
}

但我还要补充一点,从插值中调用函数并不是一个好的做法,因为它可能会导致性能问题。相反,您应该订阅设置属性的事件,然后在视图中引用该属性。这样,仅当事件触发时才会调用该函数,而不是每次渲染页面都调用。


问题在于组件上可能会有多个保存按钮。因为页面上可能会有多个表单。即使我使用了ViewChildren,我仍然需要知道哪个表单正在提交,以找出被点击的是哪个保存按钮。 - Curtis
@adam0101 我不确定问题出在哪里,也不想随意提出无法理解的建议来修补问题。至少,你的回答应该解释说 OP 的方法应该是可行的,但他们可以尝试这个解决方法... 不要声称这是一个问题的答案,因为根据你的更新,OP 的示例中按钮是可见的,但它不起作用。 - Ruan Mendes
好发现,我为什么没看到!但是一些解释为什么会很有用,值得点赞;) - AT82
当然我测试过了 :) 我没有意识到只有 ngIf 的存在会导致问题。即使使用 *ngIf=true,它也会失败,似乎是 Angular 的 bug。现在,为什么另一个按钮没有出现这个问题呢?我仍然觉得实际的问题还没有被发现。 - Ruan Mendes
因为在取消按钮中,*ngIf和事件处理clickEvent位于同一个节点<button>中。但是在第二种情况下,事件处理程序save位于外部节点<form>中(而不是具有*ngIf<button>中)- angular方式 ;). - Kamil Kiełczewski
显示剩余8条评论

2

不要在表单上使用ngSubmit事件处理程序,而是在保存按钮上使用click事件处理程序。将类型保留为submit,以便当用户使用输入控件上的Enter键提交表单时,它仍然可以工作。

<form #formElement="ngForm">
    ...
    <button #saveButton (click)="save(saveButton, formElement)" type="submit" ...>
        <mat-spinner ...></mat-spinner>
        Save
    </button>
</form>

你也可以使用对象来跟踪哪个按钮处于加载状态,从而避免需要将loading类添加到DOM中。这个示例可以正常工作。这还允许你拥有许多按钮,而无需为每个按钮创建一个固定的变量。

class FormFieldOverviewExample {
  
  hasCancel = true;
  hasSave = true;
  isLoading = {};

  saveEvent(event, buttonName, form) {
    console.log(buttonName);
    if(form.valid) {
      this.isLoading[buttonName]=true;
      // ...
    } else {
      this.isLoading[buttonName]=false;
      console.warn("Form not submitted because it contains errors");
    }
  }

  clickEvent(event, buttonName) {
    console.log(buttonName);
    this.isLoading[buttonName]=true;
    // ...
  }
}
form { display: none; }
<form #formElement="ngForm">

  ...

  <button (click)="clickEvent($event, 'cancelButton')" [disabled]="isLoading['cancelButton']" *ngIf="hasCancel" type="button" ...>
    <mat-spinner *ngIf="isLoading['cancelButton']" ...></mat-spinner>
    Cancel
  </button> 
  
  <br><br>
  
  <button (click)="saveEvent($event, 'saveButton', formElement)"  [disabled]="isLoading['saveButton']" *ngIf="hasSave" type="submit" ...>
    <mat-spinner *ngIf="isLoading['saveButton']" ...></mat-spinner>
    Save
  </button>
</form>


1
OP的方法有什么问题?在不了解问题的情况下建议解决方法是有问题的。 - Ruan Mendes
此外,如果用户使用回车键提交表单,这种方法将无法起作用...我知道,在这种情况下不会发生,但为了保险起见,您应该始终处理提交。 - Ruan Mendes
我之前尝试过这个,好像在提交时出了问题。但我刚刚又试了一下...看起来并没有发现任何问题,所以可能上次只是因为我有打字错误或者其他的 bug 之类的问题。https://stackblitz.com/edit/angular-zh7jcw-mwklob?file=app/form-field-overview-example.ts..也可以通过 Enter 按钮提交表单...虽然不能回答这个问题,但这似乎解决了我的问题。谢谢。 - Curtis

2
这实际上与提交功能无关,而与Angular和结构指令有关。当您应用结构指令(ngForngIf ...)时,Angular在幕后创建一个ng-template节点。这意味着,在结构指令范围内定义的模板引用变量可在该节点中使用。
因此,您在此处拥有:
<button #saveButton *ngIf="hasSave">
  Save
</button>

这意味着saveButton模板引用仅在为按钮创建的ng-template内部可用,因此在提交函数中不可用。

我们在这里有:

<button #cancelButton *ngIf="hasCancel" (click)="clickEvent(cancelButton)">
  Cancel
</button>

最初的回答:在按钮上使用点击事件,所以该事件可以使用“cancelButton”模板引用变量。使用“hidden”而不是其他答案中建议的“ngIf”解决了这个问题,因为我们只是隐藏了元素。

仅在该节点及其子节点中可用,而不在父节点中。 - adam0101
我没有看到这个有文档记录...你有链接吗?以下是一个示例,显示添加 *ngIf 会使该元素引用在该节点之外不可用:https://stackblitz.com/edit/angular-zh7jcw-u3eakq - Ruan Mendes
抱歉我的原始帖子没有表达清楚,但我确实知道这个解决方法。我只是不想在DOM中隐藏该元素,我正在寻找一个*ngIf仍然存在的解决方案。 - Curtis
@Curtis,我已经解释了为什么会这样,你需要使用一些其他的解决方法,例如使用ngIf和hidden。但是,由于你的代码在问题中,结构指令只是这样做的,而你无法控制它。不过很高兴你找到了适合自己的解决方案 :) - AT82
@JuanMendes 是的,这就是 Angular 和结构指令的工作原理。文档很难找到,当我在寻找时...这里有一篇我找到的博客文章:https://weblogs.thinktecture.com/thomas/2017/05/use-angular-template-reference-variables-anywhere-in-the-template-not.html - AT82

2
tl;dr 这是我的解决方案演示链接:https://stackblitz.com/edit/angular-zh7jcw-ythpdb

编辑:我刚刚发现,@AJT_82非常好地解释了OP在参考元素方面的原始问题。因此,我现在删除了以前的答案。


但是,我建议采用以下解决方案,更好地符合关注点分离采用Angular方式处理事务的要求,通过引入两个新成员cancelActivesubmitActive,您将能够更灵活地处理表单的不同状态。

您可以在此处查看它的运行情况:https://stackblitz.com/edit/angular-zh7jcw-ythpdb

export class FormFieldOverviewExample {
  cancelActive = false;
  submitActive = false;

  // this is for submit event
  saveEvent(event: Event) {
    // additional logic can be added
    // if(!this.cancelActive) return;

    this.submitActive = true;
    this.cancelActive = false;
  }

  // this is for cancel event
  cancelEvent(event: Event) {
    // additional logic can be added
    // if(!this.submitActive) return;

    this.submitActive = true;
    this.cancelActive = false;
  }

}

<form (ngSubmit)="saveEvent($event)">
  <button  class="button-with-icon"  type="button"  *ngIf="hasCancel"  mat-raised-button color="warn"  [disabled]="cancelActive"  (click)="cancelEvent($event)">
    <mat-spinner *ngIf="cancelActive" class="spinner" diameter="15"></mat-spinner>
    Cancel
  </button>
  <br><br>
  <button class="button-with-icon" type="submit" *ngIf="hasSave" mat-raised-button color="primary" [disabled]="submitActive">
    <mat-spinner *ngIf="submitActive" class="spinner" diameter="15"></mat-spinner>
    Save
  </button>
</form>

这样您还可以拥有更多的操作空间,例如:

最初的回答

<button type="submit" [disabled]="submitActive || cancelActive">...</button>

你可以简单地禁用submit按钮,而不显示它的加载动画,如果你出于某些原因想要这样做...
此外,假设您在表单中有一个<input type="text" />元素。用户通过点击或按Tab键切换到该元素,并在编辑完成后按Enter键,在这种情况下,浏览器也会自动提交表单;所以这会使您现有的代码结构变得更加复杂。
最初的回答

1
正如@AJT_82所提到的,元素引用的作用域仅限于结构指令内。您应该更新答案以包含该信息。这是一个示例,如果兄弟节点不包含*ngIf,则可以获取其引用。我没有看到这篇文章...你有链接吗?这里有一个示例,显示添加*ngIf会使元素引用在该节点外不可用:https://stackblitz.com/edit/angular-zh7jcw-u3eakq - Ruan Mendes
这需要为每个按钮设置固定变量。这不是我想要的。虽然我的演示确实有固定的变量:hasCancelhasSave。但实际上,我在代码中没有这些变量。该组件会根据 API 告诉它的数量动态生成许多按钮和表单。 - Curtis
@Curtis 谢谢你提供的信息。既然你提到的这个规范是你问题的一部分并且似乎很重要,我会在问题描述中包含它,以便其他人知道。不过我很高兴你已经解决了你的问题。 - user6576367
嘿@JuanMendes,感谢你的提示!老实说,昨天我很懒,没有阅读那些答案。相反,我写了自己的答案。无论如何,我刚刚根据你的问题更新了我的答案。至于你的问题:我没有链接,只是基于我的经验知道的。 :) - user6576367
@JuanMendes 这可能会更容易阅读,但它有一个开销成本并且不够灵活。 - Curtis
@Kenan Güler,我没有在我的帖子中包含它,因为它与我的问题无关。是的,它可以帮助其他人提出不同的解决方法,可能会在我的特定用例中有所帮助,但解决方法通常只适用于非常有限的范围。它们不一定适用于所有需要引用按钮的用例。 - Curtis

1
对于您的情况,您不应该将元素传递给函数并在其上操作类,而是应该简单地维护一个变量以保持加载状态。在您的组件中,将其初始化为:
loading: boolean = false;

在您的模板中,将此类绑定为:
[class.loading]="loading"

在您的方法中,将此属性最初设置为true,然后在异步调用完成后设置为false。类似地,使用相同的变量来设置禁用属性。
[disabled]="loading"

我正在制作一个动态组件,它可以具有多个表单和每个表单上的多个按钮。因此,通过固定变量跟踪加载哪个按钮似乎不如只向按钮的dom添加loading类效率高。如果我以这种方式做,我需要制作一个加载变量矩阵,并且必须遍历该矩阵以检查每个按钮的状态。 - Curtis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接