angularjsangularjs-directiveangularjs-ng-transclude

Transclusion directives and form


I'm trying to create few directives which would wrap layout so I can abstract from that layout (which is one of the main goals of directives as I understand it).

So what I would like to have is something like this:

<dialog>
  <dialog-title></dialog-title>
  <dialog-body></dialog-body>
  <dialog-footer></dialog-footer>
</dialog>

I have created 3 simple directives for this which look similar to this

app.directive('dialog', ()=>{
  return {
    template: '<div class="dialog" ng-transclude></div>',
    replace: true,
    transclude: true,
    restrict: 'E',
  }
})

Then I want to ensure that models defined in one directive (dialog-body) will be visible in another (dialog-footer) because I would need some form on that dialog and some nav buttons in footer that may be disabled on not depending on either that form valid or not.

  <body ng-controller="MainCtrl">
    <p>age: {{age}}</p>
    <dialog>
      <p>age: {{age}}</p>
      <dialog-body>
        <form name="dialogForm">
          <p>age: {{age}}</p>
          <input ng-model="age" minlength="3"/>
        </form>
      </dialog-body>
      <dialog-footer>
        <p>age: {{age}}</p>
      </dialog-footer>
    </dialog>
  </body>

ng-model in dialog-body will create age variable in dialog-body's scope but it would not appear in other directives untill I put it in object and declare in MainCtrl. This is how it work:

  <body ng-controller="MainCtrl">
    <p>age: {{user.age}}</p>
    <dialog>
      <p>age: {{user.age}}</p>
      <dialog-body>
        <form name="dialogForm">
          <p>age: {{user.age}}</p>
          <input ng-model="user.age" minlength="3"/>
        </form>
      </dialog-body>
      <dialog-footer>
        <p>age: {{user.age}}</p>
      </dialog-footer>
    </dialog>
  </body>

and controller:

app.controller('MainCtrl', function($scope) {
  $scope.user = {age: 1}
})

Now, I want to put a form in dialog-body. That should create FormController on dialog-body's scope, just like ng-model did (or here are some difference?). And I need to have access to it from dialog-footer to check form validity.

So after creating form in template i need to define formController in MainCtrl's scope and here is first question - how do I create instance of FormController? I thought that $scope.dialogForm = {$valid: true} should work for testing purposes and here is my final template:

  <body ng-controller="MainCtrl">
    <p>age: {{user.age}}</p>
    <p>validity: {{dialogForm.$valid}}</p>
    <dialog>
      <p>age: {{user.age}}</p>
      <p>validity: {{dialogForm.$valid}}</p>
      <dialog-body>
        <form name="dialogForm">
          <p>age: {{user.age}}</p>
          <p>validity: {{dialogForm.$valid}}</p>
          <input ng-model="user.age" minlength="3"/>
        </form>
      </dialog-body>
      <dialog-footer>
        <p>age: {{user.age}}</p>
        <p>validity: {{dialogForm.$valid}}</p>
      </dialog-footer>
    </dialog>
  </body>

Here comes main problem. When form validity changes in dialog-body it does not reflect in other directives. Why? What am I missing here?

My main target is to have directives for most used components in application so that I will have abstraction from actual layout - can this be done in different way?

Here is the plunk


Solution

  • When form validity changes in dialog-body it does not reflect in other directives. Why?

    In your directives transclude: true will create a new scope and inherit from the parent scope which in this case is the scope of MainCtrl. From what I can tell, when you declare <form name="dialogForm">, angular will bind a formController to the transcluded scope of dialogBody, i.e. for dialogBody it will do $scope.dialogForm = formController and because it is a new scope, the other transcluded scopes will not see this change.

    To fix this, you can declare a shared variable in the parent scope or use the controller as syntax which is essentially the same thing.

    <body ng-controller="MainCtrl as vm">
    

    and then bind the form to vm

        <form name="vm.dialogForm">
          <p>age: {{vm.user.age}}</p>
          <p>validity: {{vm.dialogForm.$valid}}</p>
          <input ng-model="vm.user.age" minlength="3"/>
        </form>
    

    See plunker

    Why does this work? Because all the new transcluded scopes inherit the vm from the parent scope and the formController vm.dialogForm is bound to this common variable so all the transcluded scopes will see this change.