I need to build a readmore directive in Angular2. What this directive will do is for collapse and expand long blocks of text with "Read more" and "Close" links. Not on the basis of the character count but on the basis of the specified max height.
<div read-more [maxHeight]="250px" [innerHTML]="item.details">
</div>
Can anyone please guide what would be the most reliable way to get/set the height of the element for this specific case.
Any guidelines on how this specific directive could be implemented would also be highly appreciated.
I need to build something like this https://github.com/jedfoster/Readmore.js
Solution:
With the help from Andzhik I am able to build the below component that meets my requirements.
import { Component, Input, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'read-more',
template: `
<div [innerHTML]="text" [class.collapsed]="isCollapsed" [style.height]="isCollapsed ? maxHeight+'px' : 'auto'">
</div>
<a *ngIf="isCollapsable" (click)="isCollapsed =! isCollapsed">Read {{isCollapsed? 'more':'less'}}</a>
`,
styles: [`
div.collapsed {
overflow: hidden;
}
`]
})
export class ReadMoreComponent implements AfterViewInit {
//the text that need to be put in the container
@Input() text: string;
//maximum height of the container
@Input() maxHeight: number = 100;
//set these to false to get the height of the expended container
public isCollapsed: boolean = false;
public isCollapsable: boolean = false;
constructor(private elementRef: ElementRef) {
}
ngAfterViewInit() {
let currentHeight = this.elementRef.nativeElement.getElementsByTagName('div')[0].offsetHeight;
//collapsable only if the contents make container exceed the max height
if (currentHeight > this.maxHeight) {
this.isCollapsed = true;
this.isCollapsable = true;
}
}
}
Usage:
<read-more [text]="details" [maxHeight]="250"></read-more>
If there could be any improvements, please feel free to suggest.
-
Find currentHeight inside the setTimeout(_ => { ..... }) would eliminate some window resizing issue while angular running change detections. see stackoverflow.com/questions/38930183/…chenk– chenk2016年11月20日 18:41:15 +00:00Commented Nov 20, 2016 at 18:41
10 Answers 10
I made a version that uses character length rather than div size.
import { Component, Input, ElementRef, OnChanges} from '@angular/core';
@Component({
selector: 'read-more',
template: `
<div [innerHTML]="currentText">
</div>
<a [class.hidden]="hideToggle" (click)="toggleView()">Read {{isCollapsed? 'more':'less'}}</a>
`
})
export class ReadMoreComponent implements OnChanges {
@Input() text: string;
@Input() maxLength: number = 100;
currentText: string;
hideToggle: boolean = true;
public isCollapsed: boolean = true;
constructor(private elementRef: ElementRef) {
}
toggleView() {
this.isCollapsed = !this.isCollapsed;
this.determineView();
}
determineView() {
if (!this.text || this.text.length <= this.maxLength) {
this.currentText = this.text;
this.isCollapsed = false;
this.hideToggle = true;
return;
}
this.hideToggle = false;
if (this.isCollapsed == true) {
this.currentText = this.text.substring(0, this.maxLength) + "...";
} else if(this.isCollapsed == false) {
this.currentText = this.text;
}
}
ngOnChanges() {
this.determineView();
}
}
Usage:
<read-more [text]="text" [maxLength]="100"></read-more>
7 Comments
read less still appear when word count is less than 100.. also can we have animation for expand and collapse ! how to do thatI think you'll need a Component rather then Directive. Components makes more sense since you need to add Read more button/link, i.e. update DOM.
@Component({
selector: 'read-more',
template: `
<div [class.collapsed]="isCollapsed">
<ng-content></ng-content>
</div>
<div (click)="isCollapsed = !isCollapsed">Read more</div>
`,
styles: [`
div.collapsed {
height: 250px;
overflow: hidden;
}
`]
})
export class ReadMoreComponent {
isCollapsed = true;
}
Usage:
<read-more>
<!-- you HTML goes here -->
</read-more>
5 Comments
ElementRef ngcourse.rangle.io/handout/advanced-components/elementref.html to access directly DOM element representing your Component. PS: if you are satisfied with my original answer, please accept it, thanks.ngAfterViewInit and you need to get this.el.nativeElement.offsetHeight instead of ...height. height is a member of `this.el.nativeElement.style' and has no value at that point.With the help from Andzhik I am able to build the below component that meets my requirements.
import { Component, Input, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'read-more',
template: `
<div [innerHTML]="text" [class.collapsed]="isCollapsed" [style.height]="isCollapsed ? maxHeight+'px' : 'auto'">
</div>
<a *ngIf="isCollapsable" (click)="isCollapsed =! isCollapsed">Read {{isCollapsed? 'more':'less'}}</a>
`,
styles: [`
div.collapsed {
overflow: hidden;
}
`]
})
export class ReadMoreComponent implements AfterViewInit {
//the text that need to be put in the container
@Input() text: string;
//maximum height of the container
@Input() maxHeight: number = 100;
//set these to false to get the height of the expended container
public isCollapsed: boolean = false;
public isCollapsable: boolean = false;
constructor(private elementRef: ElementRef) {
}
ngAfterViewInit() {
let currentHeight = this.elementRef.nativeElement.getElementsByTagName('div')[0].offsetHeight;
//collapsable only if the contents make container exceed the max height
if (currentHeight > this.maxHeight) {
this.isCollapsed = true;
this.isCollapsable = true;
}
}
}
Usage:
<read-more [text]="details" [maxHeight]="250"></read-more>
Comments
import { Component, Input,OnChanges} from '@angular/core';
@Component({
selector: 'read-more',
template: `
<div [innerHTML]="currentText"></div>
<span *ngIf="showToggleButton">
<a (click)="toggleView()">Read {{isCollapsed? 'more':'less'}}</a>
</span>`
})
export class ReadMoreDirective implements OnChanges {
@Input('text') text: string;
@Input('maxLength') maxLength: number = 100;
@Input('showToggleButton')showToggleButton:boolean;
currentText: string;
public isCollapsed: boolean = true;
constructor(
//private elementRef: ElementRef
) {
}
toggleView() {
this.isCollapsed = !this.isCollapsed;
this.determineView();
}
determineView() {
if (this.text.length <= this.maxLength) {
this.currentText = this.text;
this.isCollapsed = false;
return;
}
if (this.isCollapsed == true) {
this.currentText = this.text.substring(0, this.maxLength) + "...";
} else if(this.isCollapsed == false) {
this.currentText = this.text;
}
}
ngOnChanges() {
if(!this.validateSource(this.text)) {
//throw 'Source must be a string.';
console.error('Source must be a string.');
}
else{
this.determineView();
}
}
validateSource(s) {
if(typeof s !== 'string') {
return false;
} else {
return true;
}
}
}
and usage
<read-more [text]="this is test text" [maxLength]="10" [showToggleButton]="true"></read-more>
1 Comment
Again i solved these types of problem with dynamic data and full controlling.
<div class="Basic-Info-para">
<p>
<span *ngIf="personalBasicModel.professionalSummary.length>200" id="dots">
{{personalBasicModel.professionalSummary | slice:0:200}} ...
</span>
<span id="more">{{personalBasicModel.professionalSummary }}
</span>
</p>
</div>
Here , personalBasicModel.professionalSummary contain string . like any text.
slice:0:200 = use slice pipe for splice string with 200 character length . you can be change length according your requirement.
id="dots" & id="more" two important thing.
<div class="Basic-Info-SeeMore">
<button class="SeeMore"(click)="showMore(paasValueOn_SeeMoreBtn)">
{{showLess_More}}
</button>
</div>
Here we define a button with dynamic text (see more and see less ) with click event.
//---------------------------------- ts file -----------------------------------//
Define variable
showLess_More : string = "SEE MORE...";
paasValueOn_SeeMoreBtn : boolean = true;
Event(Method) fire when user click on see more button
showMore(data:boolean){
if(data){
$("#dots").css('display', 'none');
$("#more").css('display', 'inline');
this.showLess_More = "SEE LESS ...";
this.paasValueOn_SeeMoreBtn = false;
}else{
$("#dots").css('display', 'inline');
$("#more").css('display', 'none');
this.showLess_More = "SEE MORE...";
this.paasValueOn_SeeMoreBtn = true;
}
}
Comments
If you want to display the text to the maximum number of chars without cutting any word, change this line of code:
this.currentText = this.text.substring(0, this.maxLength) + "...";
To:
this.currentText = this.text.substring(0, this.maxLength);
this.currentText = this.currentText.substr(0, Math.min(this.currentText.length, this.currentText.lastIndexOf(" ")))
this.currentText = this.currentText + "..."
Comments
Just a slight improvement of @Andrei Zhytkevich code (useful for markdown)
import {
Component,
AfterViewInit,
ViewChild,
ElementRef,
Attribute,
ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'ui-read-more',
template: `
<div [class.collapsed]="isCollapsed" [style.height]="_height">
<div #wrapper>
<ng-content></ng-content>
</div>
</div>
<div class="read-more">
<button
type="button"
class="btn btn-light" (click)="onIsCollapsed()">{{isCollapsed ? 'More' : 'Less'}}</button>
</div>
`,
styles: [`
:host{
display: block;
}
.collapsed {
overflow: hidden;
padding-bottom: 1rem;
}
.read-more{
display: flex;
justify-content: flex-end;
}
`]
})
export class UiReadMoreComponent implements AfterViewInit{
@ViewChild('wrapper') wrapper: ElementRef;
isCollapsed: boolean = true;
private contentHeight: string;
private _height: string;
constructor(@Attribute('height') public height: string = '') {
this._height = height;
}
ngAfterViewInit() {
this.contentHeight = this.wrapper.nativeElement.clientHeight + 'px';
}
onIsCollapsed(){
this.isCollapsed = !this.isCollapsed;
this._height = this.isCollapsed ? this.height : this.contentHeight;
}
}
Usage
<ui-read-more height="250px">
<ngx-md>
{{post.content}}
</ngx-md>
</ui-read-more>
Comments
Thank you, I changed it up a bit for it be on NgOnInit because of the console error. There's a slight change to it and works well with Angular 6.
@Component({
selector: 'app-read-more',
template: `
<div id="textCollapse" [innerHTML]="text" [class.collapsed]="isCollapsed" [style.height]="isCollapsed ? maxHeight+'px' : 'auto'">
</div>
<a *ngIf="isCollapsible" (click)="isCollapsed =! isCollapsed">Read {{isCollapsed ? 'more':'less'}}</a>
`,
styles: [`
div.collapsed {
overflow: hidden;
}
a {
color: #007bff !important;
cursor: pointer;
}
`]
})
export class ReadMoreComponent implements OnInit {
// the text that need to be put in the container
@Input() text: string;
// maximum height of the container
@Input() maxHeight: number;
// set these to false to get the height of the expended container
public isCollapsed = false;
public isCollapsible = false;
constructor(private elementRef: ElementRef) {
}
ngOnInit() {
const currentHeight = document.getElementById('textCollapse').offsetHeight;
if (currentHeight > this.maxHeight) {
this.isCollapsed = true;
this.isCollapsible = true;
}
}
}
As you can see I changed the
const current Height = document.getElementById('textCollapse').offsetHeight;
Comments
You can use this plugin.
It is very simple to use only to pass [text] and [textLength] which you want to show by default
https://www.npmjs.com/package/nga-read-more
Comments
lineheight approach:-
lineheight and little calculation and some css text-overflow: ellipsis; does this job.
.css
.descLess {
margin-bottom: 10px;
text-overflow: ellipsis;
overflow: hidden;
word-wrap: break-word;
display: -webkit-box;
line-height: 1.8; <==== adjust line-height...a/q to your need
letter-spacing: normal;
white-space: normal;
max-height: 52px; <==== 250px etc:-
width: 100%;
/* autoprefixer: ignore next */
-webkit-line-clamp: 2; <==== clamp line 2...or 3 or 4 or 5...
-webkit-box-orient: vertical;
}
.html
<div class="col-12 rmpm">
<div id="descLess" *ngIf="seeMoreDesc === 'false'" class="descLess col-12 rmpm">
{{inputData?.desc}}
</div>
<div *ngIf="seeMoreDesc === 'true'" class="col-12 rmpm" style="margin-bottom: 10px;line-height: 1.8;">
<!--Use Line height here-->
{{inputData?.desc}}
</div>
<span class="seeMore" *ngIf="seeMoreDesc === 'false' && lineHeightDesc > 21"
(click)="updateSeeMore('seeMoreDesc', 'true')">
See More
</span>
<span class="seeMore" *ngIf="seeMoreDesc === 'true'"
(click)="updateSeeMore('seeMoreDesc', 'false')">
See Less
</span>
</div>
.ts
declare const $:any;
seeMoreDesc = 'false';
seeMore = '';
inputData = {
'desc':'Lorem Ipusme dummy text..................'
}
constructor(
private eRef: ElementRef,
private cdRef : ChangeDetectorRef
) {}
ngAfterViewChecked() {
// pass line height here
this.lineHeightDesc = (Number($('#descLess').height()) / 1.8);
this.cdRef.detectChanges();
}
public updateSeeMore(type, action) {
if (type === 'seeMoreDesc') {
this.seeMoreDesc = action;
this.cdRef.detectChanges();
} else if (type === 'seeMore') {
this.seeMore = action;
this.cdRef.detectChanges();
}
}
Comments
Explore related questions
See similar questions with these tags.