如何在AngularJS中访问父控制器或作用域中的FormController?

16

我有一个页面上有多个表单,但我只想一次显示一个表单。为此,我将每个表单分成一个部分,并使用 Bootstrap 的手风琴插件,只允许一个部分打开。

我的标记大致如下:

<a ng-click="open_section('section1')">Section 1</a>

<div collapse="section1">
  <form name="section1Form">
  </form>
</div>

<a ng-click="open_section('section2')">Section 2</a>

<div collapse="section2">
  <form name="section2Form">
  </form>
</div>

一切进展顺利,我可以在表格之间导航等。

因为我不希望用户在当前正在编辑的部分包含验证错误时打开另一个部分,所以我尝试在open_section函数中检查与其关联的表格是否有效。

我试过了,但是无法访问控制页面的表单相关联的FormController。由于某种原因,它们没有发布到scope上。

这是我尝试的:

  • $scope.section1Formundefined

  • 尝试使用$scope.$watch('section1Form, function(){}),仍然是undefined

  • 尝试将表单名称作为第二个参数添加到open_section中,像这样:open_section('section1', section1Form),但在函数中第二个参数是undefined

<form></form>标签之间,我可以访问FormController,但在它们之外我就不能。由于事件来自<form>之外(部分的打开、关闭),我无法将FormController传递给我的控制器来检查我的表单的有效性。

有没有办法绕过这个问题,或者我应该重构我的页面?

顺便说一下,我正在使用Angular 1.1.5。

另外,通过AngularJS Batarang Chrome插件检查,我可以看到表单作为子scope发布到当前scope。

编辑:此应用程序的范围层次结构如下所示:

 - root
 |
 ---current controller\'s scope
 |
 ----scope that contains the forms

这是因为我在使用ng-include吗?那么在控制器中没有访问那些表单的途径了吗?


当您尝试检查$scope.section1Form时,它并不会立即可用。您可以尝试在表单的某个事件上进行检查,比如将ng-click添加到某个元素上。 - Chandermani
如果我尝试从表单中发送它,例如通过ng-click="something(nameOfForm)"提交事件,我可以使用它。但是如果事件不发生在表单内部,则我无法使用它。 - adamors
10个回答

16

为了处理动态表单,以及相关的FormController的动态位置,我使用了一个简单的指令来帮助定位包含表单的作用域。

解决方案:

创建一个指令,该指令 $emit 表单关联的作用域:

  module.directive('formLocator', function() {
    return {
      link: function(scope) {
        scope.$emit('formLocator');
      }
    }

在标记中使用该指令:

<form name="myForm" novalidate form-locator>

在您的控制器中监听指令广播的事件:

$scope.$on('formLocator', function(event) {
  $scope.myDeeplyNestedForm = event.targetScope.myForm;
});

我们可以通过使用Angular的自然作用域遍历来实现此类型的事件发射功能,就像我在下面的答案中概述的那样https://dev59.com/3WIk5IYBdhLWcg3wl_Ap#24495707 - Abhinav Gujjar
这非常优雅。我应该更经常考虑在Angular指令中使用事件发射。 - Semicolon
这是一个很好的方法!然而,https://dev59.com/3WIk5IYBdhLWcg3wl_Ap#22811273中提出的答案也可以实现同样的效果(将表单放在父级作用域中),但避免了使用事件和创建自己的指令。 - Mike Ohlsen
感谢您提供如此高雅的解决方案,您让我的一天变得更美好了。 - Jalal El-Shaer

7

使用 Angular 的作用域层级查找过程,可以更简单地沿着作用域层级向上进行通信。

这使得我们可以使用点符号将来自较低作用域的对象附加到较高作用域中。

在这种情况下,您需要在顶级控制器上创建一个“catcher”空对象。然后,可以将来自较低作用域的表单对象分配给此 catcher 对象。这是一个演示的 plunk。

http://plnkr.co/edit/xwsu48bjM3LofAoaRDsN?p=preview

虽然不是完美优雅的,但如果你把这个“catcher”对象看作事件监听器,我们仍然遵循标准模式。

在控制器中创建一个空的“catcher”对象,以便在其中引用嵌套表单。

function mainController($scope){
  $scope.catcher = {

  };
 }

然后在标记中,无论何时声明 ng-form 指令,都要设置 catcher.formName = formName,如下所示:

<ng-form name="alpha">
          <span ng-init="catcher.alpha = alpha"></span>
          <input type="text" required="" ng-model="alphaValue" />
        </ng-form>

因为我们被分配到了“catcher。”,所以Angular会沿着作用域层次结构向上遍历,并在mainController中找到它,而不管它们之间有多少个控制器(当然,假设它们也没有catcher对象)。

当没有其他选择时,这很有帮助,但这会降低应用程序的可测试性,在大多数情况下,我认为https://dev59.com/3WIk5IYBdhLWcg3wl_Ap#24495707是最好的选择。 - Brian
这比已接受的解决方案(始终尝试避免事件)要简单得多,更优雅。不过似乎没必要使用“catcher”。使用controllerAs / vm语法,您只需执行vm.alpha = alpha即可。 - Jimmy

5
因为我在使用带有ng-view的部分视图,表单被注册在我的控制器的子作用域中。由于原型继承的关系,似乎我无法从父级作用域中访问子作用域。
也就是说,通过将表单控制器实例通过负责打开/关闭手风琴的函数传递到我的控制器中,我成功地将其获取到了。
解决方案类似于这样:
<a ng-click="open_section('section1', section1Form)">Section 1</a>

<div collapse="section1">
  <form name="section1Form">
  </form>
</div>

<a ng-click="open_section('section2', section2Form)">Section 2</a>

<div collapse="section2">
  <form name="section2Form">
  </form>
</div>

1
我看到了同样的事情。但是它仍然没有回答如何自动将formController引入作用域的问题(正如所宣传的那样)。 - cc young
无法完成,因为如果你使用ng-view,你基本上是在子作用域中,而不是控制器正在使用的作用域中。这相当令人困惑,特别是因为它没有被充分记录。 - adamors

4
AngularJS文档中,可以阅读到以下内容:
<form
       [name="{string}"]>
</form>

表单名称。如果指定,则表单控制器将在相关作用域中发布,使用该名称。

然而,有一些指令(如ngIf)会创建一个新的作用域:

请注意,当使用ngIf删除元素时,它的作用域将被销毁,并且在元素恢复时将创建一个新的作用域。

这可能是您的情况吗?如果是这样,您可以尝试将表单名称设置为“forms.section1Form”,然后在作用域中相应地访问它。


在每种情况下,表单都被命名,就像我在示例标记中所说的那样。手风琴是否会创建和销毁作用域? - adamors
尝试使用 forms.section1Form,仍然得到 undefined - adamors
我的使用情况可能不同,但是将名称添加到表单中使得FormController实例在我的控制器的$scope中可用。 - findzen

4

要从指令、控制器等访问表单,您可以使用ng-init:

例如:

  <form name="myForm">
     <div ng-init="myLocalScopeVar=form"></div

    <input name="myField" ....>
  </form>

你可以访问表单数据,或者如果这是一个指令,可以通过绑定变量返回。以下是上述模板的控制器示例:
     if ($scope.myLocalScopeVar.myField.$valid ) ...

这与 Sinil D的回答相似,它将表单保存在父$scope中,但它不使用事件。本质上,它正在做同样的事情,但开销较小。

我猜您所说的“myLocalScopeVar=form”实际上是指“myForm”,如果是这样的话,您需要写两次表格的名称... 这并不太好。 - lqbweb

3
为了获取某个指令实际负责的FormController,你只需使用带有脱字符前缀的form进行require。这样可以在指令的链接函数中存储一个引用:
module.directive('awesomeFormChild', [function () {

    var formController;

    return {
        require: '^form',
        link: function(scope, elm, attr, ctrl) {
            formController = ctrl;
        }
    };

}])

2

你的控制器可能在表单之外,或者在表单填充到作用域之前就尝试获取表单。

我创建了一个PlunkR并且它工作得很好。


谢谢,但那只是在创建这个问题时的一个打字错误。对此我很抱歉。我的实际代码没有这些错别字。 - adamors
我只在$routeProvider中设置页面使用的控制器而不在实际模板中设置,这样会有影响吗? - adamors
不,它应该与$routeProvider一起工作。您是否使用ng-view来呈现您的路由? - L105
谢谢您提供Plunkr,我之前不明白为什么在控制台记录$scope时可以看到表单列表,但每次尝试访问$scope.formName对象时会得到未定义。 - m.e.conroy

2
关于Sunil D.提供的将表单范围提供给父作用域的新颖解决方案(请参见https://dev59.com/3WIk5IYBdhLWcg3wl_Ap#22487840),您实际上可以从在$emit调用中发送的事件对象中获取表单的作用域。
自至少Angular 1.0.3以来,事件对象具有targetScope属性,该属性引用了触发事件的作用域。 (有关事件对象属性的更多信息,请参见http://docs.angularjs.org/api/ng/type/$rootScope.Scope#$on。)
考虑到这一点,您可以将Sunil D.的指令代码简化为:
module.directive('formLocator', function() {
  return {
    link: function(scope) {
      scope.$emit('formLocator');
    }
  };
});

模板完全没有改变:
<form name="myForm" novalidate="" form-locator>

接下来,您需要更改事件处理代码:

$scope.$on('formLocator', function(event) {
  $scope.myDeeplyNestedForm = event.targetScope.myForm;
});

这个指令在任何需要将作用域提供给其祖先的场景中都是相关的,而不仅仅是在ngForm的情况下。在我的代码库中,我将这个指令重命名为更通用的“present”,因为思考该指令目的的一种方式是宣布作用域的存在。


0

与Sunil D.的解决方案类似,我遇到了一个类似的问题,但是隐式地将表单控制器(formController)设置为作用域变量,因为我不想两次硬编码表单的名称:

renderFormModule.directive('formLocator', function() {
    return {
        link: function(scope, element) {
            scope.formAssociated = scope[element[0].name];
        }
    }
});

然后,视图会像这样:

    <form name="testForm" ng-controller='formController as formCtrl' form-locator novalidate>

0

我知道这会引起很多恐慌...但我相当懒,使用了以下方法(我希望它不会因为被添加为监视并在每个digest中执行而产生巨大的开销):

<form name = "formName" nf-if="someCondition">

{{(controller.referenceToForm==undefined && (controller.referenceToForm=formName) != undefined)?'':''}}

</form>

然后我只需在我的控制器中使用controller.referenceToForm等。


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