简体   繁体   中英

How do I access FormController in parent controller or scope in AngularJS

I have a page with multiple forms and I only want to show one at a time. For this I separated each form into a section and using Bootstrap's accordion plugin I only allow one open section at a time.

My markup looks something like this:

<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>

Everything work just fine, I can navigate between the forms etc.

Because I don't want the user opening a section if the one they are currently editing contains validation errors, I tried checking in the open_section function if the form associated with it is valid or not.

I tried, but I could not. I could not access the FormController associated with the forms in the controller that is responsible for the page. For some reason, they are not getting published on the scope.

This is what I tried:

  • $scope.section1Form is undefined

  • tried with $scope.$watch('section1Form, function(){}) , still undefined

  • tried adding the name of the form as a second parameter to open_section like so: open_section('section1', section1Form) but in the function the second argument is undefined .

Between the <form></form> tags, I have access to the FormController, but outside them I don't. Since the event is coming from outside the <form> (the closing, opening of the sections) I can't pass the FormController to my controller to check the validity of my forms.

Is there a way to get around this, or should I refactor my page?

I am using Angular 1.1.5 btw.

Also, checking with the AngularJS Batarang Chrome plugin, I can see that the forms get published as child scopes to the current scope.

EDIT : this is how the scope hierarchy looks for this app

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

Is this because I'm using ng-include ? Is there no way to access those forms in a controller then?

To deal with dynamic forms, and thus the dynamic location of the associated FormController, I used a simple directive to help locate the scope that contains the form.

Solution:

Create a directive that $emit's the scope associated w/the form:

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

Use the directive in the markup:

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

Listen for the event broadcast by the directive in your controller:

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

There a much simpler way to communicate back up in the scope hierarchy by using angular process of looking for objects up in its scope hierarchy.

This allows us to attach the object from a lower scope into a higher scope by using the dot notation.

In this case you need to create a "catcher" empty object on the top level controller. This catcher object can then be assigned the form objects from the lower scopes. Here is a plunk for demo.

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

It isn't perfectly elegant but if you think of this "catcher" object as an event listener, we're still following a standard pattern.

create an empty catcher object in the controller where you'd like a reference to the nested form

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

  };
 }

Then in the markup itself whenever the ng-form directive is declared, set catcher.formName = formName like so

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

Because we're assigned to "catcher.", angular will traverse up the scope hierarchy and find thd it in the mainController regardless of any number of controllers in between (assuming they also don't have a catcher object, of course)

Because I was using partial views with ng-view the forms were registered on a child scope of my controller. It seems that I cannot access the child scopes from a parent one, due to prototypical inheritance.

That said, I did manage to get the form controller instances into my controller by passing them through the function that was responsible for opening/closing the accordion.

The solution is something like this:

<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>

In the angular documentation one can read:

<form
       [name="{string}"]>
</form>

Name of the form. If specified, the form controller will be published into related scope, under this name.

However, there are certain directives, like ngIf , that create a new scope:

Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored.

Can that be your case? If so, you can try setting the form name to something like "forms.section1Form" and then access it accordingly in the scope.

To access the form from a directive, controller, etc... you can utilize ng-init:

For example:

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

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

You know can access the form data, or pass back with a bound variable if this is a directive. Example in the controller for the above template:

     if ($scope.myLocalScopeVar.myField.$valid ) ...

This is similar to Sinil D's answer great answer, in that is saves the form onto the parent $scope, but it doesn't use events. It is essentially doing the same thing, but a bit less overhead.

To get the actual FormController responsible for a certain directive, you simply require the form with the hat prefix. This enables to store a reference in your directives' link function:

module.directive('awesomeFormChild', [function () {

    var formController;

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

}])

Your controller may be outside your form OR your are trying to get your form before the form is being populated in scope.

I created a PlunkR and it working well.

Regarding Sunil D.'s novel solution to providing the form's scope to parent scopes (see https://stackoverflow.com/a/22487840/1518575 ), you can actually get the form's scope from the event object that is sent in an $emit call.

Since at least Angular 1.0.3, the event object has a targetScope property, which references the scope on which the event was $emit-ed. (See http://docs.angularjs.org/api/ng/type/ $rootScope.Scope#$on for more information about the properties of the event object.)

With this in mind, you can simplify Sunil D.'s directive code to be:

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

The template doesn't change at all:

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

And subsequently you would change the event handling code to be:

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

This directive would be relevant in any scenario where you would want to provide a scope to one of its ancestors, not just in the case of ngForm. In my codebase, I've renamed this directive to the more generic 'present', since one way to think of the directive's purpose is that it announces the presence of a scope.

I know this will cause a lot of horror.... but I was quite lazy and used the following (which I'd like to believe does not create massive overhead due to being added as a watch and excecute every digest):

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

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

</form>

Then I just use controller.referenceToForm within my controller, etc.

Similar to Sunil D. solution, I have come accross a similar one, but implicitly setting to the scope variable, the formController in which the tag was used on, as I did not want to hard code the name of the form twice:

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

And then, the view something like:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM