Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacing parts of directive template when their respective transclusion content is present

I'm trying to do the following:

If I add <my-custom-directive></<my-custom-directive>

it should expand to

<div class="my-custom-container">
   <label class="my-custom-label">Fallback</label>
   <input class="my-custom-input"/>
</div>

which can be done by setting the above as template and replace:true in DDO.

If I add the following in HTML:

<my-custom-directive>
   <my-custom-label class="users-custom-class"><span>Custom content</span><my-custom-label>
</<my-custom-directive>

it should expand to

<div class="my-custom-container">
   <label class="my-custom-label users-custom-class"><span>Custom content</span></label>
   <input class="my-custom-input"/>
</div>

Which means if the user wants to provide custom <label>, <input> etc, we use transclusion, and the transcluded content replaces respective slot in the original template, similar to how a replace:true directives's would replace itself with it's template.

I'm not able to combine the replace and transclusion functionality.


What I've so far (something-working-state) is the following:

angular.module('test', [])
  .directive('transTest', function() {
    return {
      transclude: {
        lab: '?labelTest',
        inp: '?inputTest'
      },
      replace: true,
      template: '<div class="container"><label ng-transclude="lab">Fallbacl label</label><input type="text" placeholder="fallback" ng-transclude="inp"></div>',
      link: function(scope, element, attrs, ctrl, transclude) {
        console.log(transclude())
      }
    }
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.2/angular.js"></script>
<div ng-app="test">
  <div trans-test class="test">
    <label-test>test label</label-test>
    <input-test>test input</input-test>
  </div>
</div>

As you can see, the trancluded content goes inside the translude containing element, instead of replacing it. I've read the source code comments, articles and also checked the implementation of ui-bootstrap-accordion and tried my luck with transclude:'element', but it leaves nothing in DOM but a comment.

transclusion, replace etc are the available options that I've found which offers functionality similar to what I'm trying to achieve. But they don't seem to play well toghether. What is the correct way to achieve this kind of functionality in angular, if possible..?

like image 606
T J Avatar asked Mar 25 '16 19:03

T J


People also ask

Which directive definition option is used to replace the current element if true?

As the documentation states, 'replace' determines whether the current element is replaced by the directive. The other option is whether it is just added to as a child basically.

Which directive do we use to inform AngularJS about the parts controlled by it?

The ngRef attribute tells AngularJS to assign the controller of a component (or a directive) to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM element to the scope. The ngRepeat directive instantiates a template once per item from a collection.

What is Transclusion in Javascript?

transclude - compile the content of the element and make it available to the directive. Typically used with ngTransclude. The advantage of transclusion is that the linking function receives a transclusion function which is pre-bound to the correct scope.


2 Answers

It seems I finally made it work. The solution is made of 2 parts:

  1. Define directives for the transclude-slot elements with template similar to the fallback (default).

    The major reason for doing this is to make use of angular's built in capability to copy attributes to templates root element when replace:true is set in DDO. I didn't wanted to do it manually in the link function.
    Another reason is that it lets you add additional features such as transclusion which is not necessary in default template

  2. The second step is to not define ng-transclude directive in the template, instead use the transclude function passed to link for accessing the trancluded content of various slots, and replace the respective element with the transcluded content if it is present (using transclude.isSlotFilled())

Well, this wasn't easy to get my mind around, and it isn't easy to explain as well. Hope the demo below explains it better than words:

angular.module('test', [])
  .directive('transTest', function() {
    return {
      replace: true,
      transclude: {
        lab: '?labelTest',
        inp: '?inputTest'
      },
      template: '<div class="test-parent"><label class="fallback-label">Fallback </label><br><input type="text" class="fallback-input"></div>',
      link: function(scope, element, attrs, ctrl, transclude) {
        if (transclude.isSlotFilled('lab')) {
          var label = transclude(angular.noop, null, 'lab');
          element.find('label').replaceWith(label);
        }
        if (transclude.isSlotFilled('inp')) {
          var input = transclude(angular.noop, null, 'inp');
          element.find('input').replaceWith(input);
        }
      }
    }
  }).directive('labelTest', function($compile) {
    return {
      template: '<label class="fallback-label ng-transclude">Fallback </label>',
      replace: true,
      transclude: true
    }
  }).directive('inputTest', function($compile) {
    return {
      template: '<input type="text" class="fallback-input">',
      replace: true
    }
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.2/angular.js"></script>
<div ng-app="test">
  <div trans-test class="test">
    <label-test class="custom-label">Custom content</label-test>
    <input-test class="custom-input" placeholder="custom"></input-test>
  </div>

  <br>
  <br>
  <div trans-test class="test">
  </div>
</div>
like image 135
T J Avatar answered Sep 27 '22 18:09

T J


You might be looking for a more elegant solution, but you can tap into the directive's controller $transclude function to find out if a transclusion-slot has been filled.

controller: function($transclude, $scope) {
  $scope.fallback = !$transclude.isSlotFilled('lab');

Then, use that information to build your template.

template: '<div class="container">\
  <label ng-if="fallback === true">Fallback label</label>\
  <div ng-if="fallback === false" ng-transclude="lab"></div>\

However, if you can fully define the content being transcluded,

<label-test>
    <label>test label</label>
</label-test>

it might make more sense for the destination content to be replaced - rather than the entire element:

template: '<div class="container">\
  <div ng-transclude="lab">\
    <label>Fallback label</label>\
  </div>\

Here is a plunker showing both approaches: http://plnkr.co/edit/EEq5vovFrSW7kG81yWuf?p=preview

like image 26
j.wittwer Avatar answered Sep 27 '22 17:09

j.wittwer