0

I have a div on which I have a directive that binds HTML content and compile it (sort of ng-bing-html directive, but that also compile html to allow insertion of custom directives). The HTML code looks like this :

<div ng-repeat="text in texts">
     <div class="content-display" 
          bind-html-compile="text | filterThatOutputsHTMLCodeWithCustomDirectives | nl2br">
     </div>
</div>

The problem is I need to display only a restricted portion of each of the content-display divs, and have a "read more..." button that would expand the corresponding div to its full size. But I CANNOT truncate the text bound in the div, since it's not only text, but can contain HTML tags/directives.

I found this JQuery code, that accomplish what I want visually : https://stackoverflow.com/a/7590517/2459955 (JSFiddle here : http://jsfiddle.net/rlemon/g8c8A/6/ )

The problem is that it's not Angular-compliant, and is pure JQuery. And since my div in which I bind the HTML content is inside an ng-repeat... this solution wouldn't work when the texts array gets refreshed asynchronously.

Do you see a way to have the same behavior as in the answer linked earlier, but being more "Angular compliant" and applying it automatically to each of the content-display divs added by the ng-repeat ?

2
  • Didn't try it myself, but have you seen: gist.github.com/mrzmyr/706d5322d91956308bd8 ? Commented Dec 14, 2015 at 11:21
  • It truncates the text... so it won't work because I have HTML inside the div, not only text. So I can not truncate it. Commented Dec 14, 2015 at 11:29

5 Answers 5

1

Consider using a CSS approach like the one described here: https://css-tricks.com/text-fade-read-more/

CSS:

.sidebar-box {
  max-height: 120px;
  position: relative;
  overflow: hidden;
}
.sidebar-box .read-more { 
  position: absolute; 
  bottom: 0; 
  left: 0;
  width: 100%; 
  text-align: center; 
  margin: 0; padding: 30px 0; 

  /* "transparent" only works here because == rgba(0,0,0,0) */
  background-image: linear-gradient(to bottom, transparent, black);
}

Rather than use jQuery for the read more "reveal", you could create an AngularJS directive for the read more button.

Directive (untested):

angular.module('myApp')
    .directive('readMore', readMoreDirective);

function readMoreDirective() {
    return function(scope, iElement) {
        scope.$on('click', function() {
            var totalHeight = 0;
            var parentElement = iElement.parent();
            var grandparentElement = parentElement.parent();
            var parentSiblings = grandparentElement.find("p:not('.read-more')");

            // measure how tall inside should be by adding together heights
            // of all inside paragraphs (except read-more paragraph)
            angular.forEach(parentSiblings, function(ps) {
                totalHeight += ps.outerHeight();
            });

            grandparentElement.css({
                // Set height to prevent instant jumpdown when max height is removed
                height: grandparentElement.height(),
                'max-height': 9999
            })
            .animate({
                height: totalHeight
            });
        });
    };
}
Sign up to request clarification or add additional context in comments.

3 Comments

I think you forgot the part of the article named "The Reveal with JQuery" where it says it uses JQuery to do the "expand" animation ! It's not only CSS. And the problem would then be exactly the same as with the answer I linked in my question : when more divs are added by the ng-repeat, the JQuery binding won't be added.
I found this solution that might be a workaround to the click event not being binded to newly inserted DOM elements : stackoverflow.com/a/18876600/2459955
Sorry, was trying to answer on my iPhone but copy/pasting code is a nightmare. I will update to show how the read more button can be done in Angular.
1

One clean way would be using a class for truncated div, and remove it to display all the text :

Angular scope :

$scope.truncated = []; // make new array containing the state of the div (truncated or not)
for(var i; i < texts.length -1; i++){
    $scope.truncated.push(0); // fill it with 0 (false by default)
}
$scope.textTruncate = function(index) {
    $scope.truncated[index] = !$scope.truncated[index]; // toggle this value
}

Angular view :

<div ng-repeat="text in texts" ng-class="{truncated: truncated[$index]}">
    <div class="content-display" 
          bind-html-compile="text | filterThatOutputsHTMLCodeWithCustomDirectives | nl2br">
     </div>
    <button ng-click="textTruncate($index)" >Read more</button>
</div>

CSS :

.content-display {
    max-height: 1000px; /* should be your max text height */
    overflow: hidden;
    transition: max-height .3s ease;
}
.truncated .content-display {
    max-height: 100px; /* or whatever max height you need */

}

That is what comes in my mind, not sure if it's the most efficient way.

7 Comments

This answer seems even easier than @shaun-scovil 's one and would work even if content is not inside <p> tags (which is not desired). Would it be easy to add an animation between the two different sizes with ng-animate ? I never used it.
Actually, as it uses CSS classes, CSS transitions would do the job easily div .content-display (transition: max-height .3s ease).
Your solution is working fine. I just think you misplaced the "ng-class", it should be on the content-display div. But I didn't manage to make the transition work. Adding the transition CSS property to the ".content-display" class has no effect. Also, would it be easy to add a fade effect at the bottom of the truncated div ? (as seen on the example I linked in my question)
Adding an arbitraty "max-height" to the .content-display class allows the transition to be seen. But we should calculate the correct max-height, otherwise the transition-timing (and easing) is wrong.
Edited my answer with transition. I prefer to put that kind of classes on the container itself (e.g. an article - .content-display's parent instead of .content-display itself). Why ? If I need more styling, I could go all the way with .truncated .children (more reliable than .truncated + button + span, for example).
|
0

Try using <p data-dd-collapse-text="100">{{veryLongText}}</p> inside the ng-repeat

Documentation Here

1 Comment

It won't work, because it creates two separate spans with truncated text inside.
0

Finally, I ended up using the approach given in this answer with a slight modification : https://stackoverflow.com/a/7590517/2459955

Indeed, since I have a ng-repeat adding more divs into the DOM, the $elem.each() function wouldn't trigger for these additional divs. The solution is to use a JQuery plugin called jquery.initialize.

This plugin gives an $elem.initialize() function that has exactly the same syntax as $elem.each() but initialize() will call the callback again on new items matching the provided selector automatically when they will be added to the DOM. It uses MutationObserver.

The final code looks like this. I have some JQuery code in my module.run() entry (run once at module initialization):

var slideHeight = 400;
$(".content-collapse").initialize(function() {
  var $this = $(this);
  var $wrap = $this.children(".content-display");
  var defHeight = $wrap.height();
  if (defHeight >= slideHeight) {
    var $readMore = $this.find(".read-more");
    var $gradientContainer = $this.find(".gradient-container");
    $gradientContainer.append('<div class="gradient"></div>');
    $wrap.css("height", slideHeight + "px");
    $readMore.append("<a href='#'>Read more</a>");
    $readMore.children("a").bind("click", function(event) {
      var curHeight = $wrap.height();
      if (curHeight == slideHeight) {
        $wrap.animate({
          height: defHeight
        }, "normal");
        $(this).text("Read less");
        $gradientContainer.children(".gradient").fadeOut();
      } else {
        $wrap.animate({
          height: slideHeight
        }, "normal");
        $(this).text("Read more");
        $gradientContainer.children(".gradient").fadeIn();
      }
      return false;
    });
  }
});

And the corresponding HTML (cleaned for demonstration purpose):

 <div class="content-collapse" ng-repeat="text in texts">
    <div class="content-display" bind-html-compile="::text"></div>
    <div class="gradient-container"></div>
    <div class="read-more"></div>
 </div>

This solution allows for smooth expand/collapse animation that works fine without any CSS hack, it adds the "Read more" button only on answers that exceeds the desired size limit, and works even if the texts array is modified by asynchronous requests.

Comments

0

I had a similar issue. I had o implement this for a data table. I found following directive and it worked smoothly as per requirements:-

Ui Framework- Angular js

In Html

  <tr data-ng-repeat="proj in errors">
        <td dd-text-collapse dd-text-collapse-max-length="40" 
        dd-text-collapse-text="{{proj.description}}"></td> 

in Javascript:-

    app.directive('ddTextCollapse', ['$compile', function($compile) {


      return {
          restrict: 'A',
          scope: true,
          link: function(scope, element, attrs) {


              /* start collapsed */
              scope.collapsed = false;


              /* create the function to toggle the collapse */
              scope.toggle = function() {
                  scope.collapsed = !scope.collapsed;
              };


              /* wait for changes on the text */
              attrs.$observe('ddTextCollapseText', function(text) {


                  /* get the length from the attributes */
                  var maxLength = scope.$eval(attrs.ddTextCollapseMaxLength);


                  if (text.length > maxLength) {
                      /* split the text in two parts, the first always showing */
                      var firstPart = String(text).substring(0, maxLength);
                      var secondPart = String(text).substring(maxLength, text.length);


                      /* create some new html elements to hold the separate info */
                      var firstSpan = $compile('<span>' + firstPart + '</span>')(scope);
                      var secondSpan = $compile('<span ng-if="collapsed">' + secondPart + '</span>')(scope);
                      var moreIndicatorSpan = $compile('<a ng-if="!collapsed">... </a>')(scope);
                      var lineBreak = $compile('<br ng-if="collapsed">')(scope);
                      var toggleButton = $compile('<a class="collapse-text-toggle" ng-click="toggle()">{{collapsed ? "(less)" : "(more)"}}</a>')(scope);


                      /* remove the current contents of the element
                       and add the new ones we created */
                      element.empty();
                      element.append(firstSpan);
                      element.append(secondSpan);
                      element.append(moreIndicatorSpan);
                      element.append(lineBreak);
                      element.append(toggleButton);
                  }
                  else {
                      element.empty();
                      element.append(text);
                  }
              });
          }
      };
       }]);

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.