4

I'm trying to create a directive that will load the page and make it available in a service and as scope and it's going to have content inside the directives element.

This should be pretty self-explaining (simplified) what I try to do:

<cms-page page-id="829">
    <h1>Testing</h1>
    <ul ui-sortable ng-model="pageData.sections.main">
        <li ng-repeat="element in pageData.CmsPage.sections.main track by $index">
            <div ng-include="'/templates/cms/elements/' + element.element_type + '.html'"></div>
        </li>
    </ul>
    <pre>{{pageData | json}}</pre>
</cms-page>

The issue with that is now that it doesn't show the {{pageData}}. How can I create a directive that will show the existing markup and parse the existing markup and child directives?

Here is my directive:

angular.module(cms).directive('cmsPage', ['CmsPage', 'CmsPagesService', function(CmsPage, CmsPagesService) {
    return {
        restrict: 'E',
        transclude: true,
        template: '<div ng-transclude></div>',
        scope: {
            pageId: '@pageId'
        },
        controller: function($scope) {
            $scope.pageData = {};
            CmsPagesService.get($scope.pageId).then(function(result) {
                if (result.status == 'success') {
                    $scope.pageData = result.data;
                } else {
                    throw 'Page failed to load.';
                }
            });
            $scope.$watch('pageData', function() {
                CmsPage.setPage($scope.pageData);
            });
        }
    };
}]);
1
  • Could you do something like this? <div ng-include ng-src="getPartial()"></div> Commented Dec 24, 2014 at 2:13

2 Answers 2

1

Your problem comes from the scope to which your transcluded content is bound. It might appear as though the html within your <cms-page> tag inherits the isolated scope on the cms-page directive — a fair assumption given that it's nested within it — but in actuality it does not. Transcluded content will, by default, respect the scope from which it was taken.

The consequence of such is that, in practice, you have two scopes in parallel - the isolated scope on the directive on which pageData exists, and the original scope governing your markup where pageData is undefined (and it's this scope that your transcluded html is bound to). There are two approaches you could take to rectify this:

Directive template

As presented, there appears to be little reason why transclusion is needed here. This seems like a fairly regular directive whereby the directive html can exist on the directive itself:

HTML:

<cms-page page-id="829"></cms-page>

Directive:

template: // the original html to be transcluded

This has a few advantages:

  • if the directive is needed in more than one place, the nested markup need not be repeated
  • pageData is inherently bound to the correct scope
  • your markup is more intuitive; it would be confusing to anyone unfamiliar with your code where pageData comes from by looking at your original html

If a variable template is required, you can select one dynamically by using the template function:

HTML:

<cms-page page-id="829" page-template="templateA"></cms-page>

Directive:

template: function(tElem, tAttrs) {
  if(tAttrs.pageTemplate) {
    return '<div>...</div>';
  }  // and so on...
}

Manual Transclusion

If for some reason you cannot use a directive template, then instead of having angular transclude your content, which it does by associating it with the scope from which it was taken, you can transclude the content yourself and point it to whichever scope you please. The transclude function is available as the fifth argument of the link function:

transclude: true,
template: '<div></div>',
scope: {
  pageId: '@'
}
link: function(scope, elem, attrs, ctrl, transclude) {
  transclude(scope, function(clone) {
    // transcluded html is now bound to the isolated scope of the directive
    // therein making pageData accessible "externally"
    elem.append(clone);
  });
}
Sign up to request clarification or add additional context in comments.

Comments

1

As mentioned in the AngularJS $compile documentation, regarding transclusion:

Transclusion is the process of extracting a collection of DOM element from one part of the DOM and copying them to another part of the DOM, while maintaining their connection to the original AngularJS scope from where they were taken.

This suggests that the transcluded element only has access on the scope from which it was taken from. If you want to have pageData available, then you'd have to instruct your directive's scope definition to provide two-way data binding towards pageData.

DEMO

  .directive('cmsPage', ['CmsPage', 'CmsPagesService', function(CmsPage, CmsPagesService) {
    return {
          restrict: 'E',
          transclude: true,
          template: '<div ng-transclude></div>',
          scope: {
              pageId: '@pageId',
              pageData: '='
          },
          controller: function($scope) {
              $scope.pageData = {};
              CmsPagesService.get($scope.pageId).then(function(result) {
                $scope.pageData = result.data;
              });
              $scope.$watch('pageData', function() {
                  CmsPage.setPage($scope.pageData);
              });
          }
      };
  }]);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.