Directive for Angular 2: Expand Further

Looking to create a custom readmore directive in Angular2 that will collapse and expand long blocks of text based on a specified max height, rather than character count. The directive will include "Read more" and "Close" links.

<div read-more [maxHeight]="250px" [innerHTML]="item.details">
</div>

Seeking advice on the most reliable method for getting/setting the height of the element in this scenario.

Any suggestions or guidelines on how to implement this directive would be greatly appreciated.

Similar functionality is needed to what can be seen here: https://github.com/jedfoster/Readmore.js

Solution:

Thanks to Andzhik's help, I have managed to create the following component that fulfills 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 you have any suggestions for improvement, please feel free to share.

Answer №1

My latest adaptation focuses on character count instead of the size of the div container.

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();       
    }
}

How to utilize:

<read-more [text]="text" [maxLength]="100"></read-more>

Answer №2

If you're looking to implement a feature with a "Read more" button, it's best to use a Component instead of a Directive. Components are more suitable for this purpose as they allow for easier manipulation of the 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;
}

To use this component:

<read-more>
   <!-- Your HTML content here -->
</read-more>

Answer №3

Thanks to the expertise of Andzhik, I was able to create a customized component that perfectly aligns with my needs.

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>

Answer №4

import { Component, Input,OnChanges} from '@angular/core';
@Component({
    selector: 'read-more',
    template: `
        <div [innerHTML]="currentContent"></div>
        <span *ngIf="displayToggleButton">
            <a (click)="toggleView()">{{isCollapsed? 'Read more':'Read less'}}</a>
        </span>`
})

export class ReadMoreDirective implements OnChanges {

    @Input('content') content: string;
    @Input('maxLength') maxLength: number = 100;
    @Input('displayToggleButton') displayToggleButton:boolean;

    currentContent: string;
    public isCollapsed: boolean = true;

    constructor() {}

    toggleView() {
        this.isCollapsed = !this.isCollapsed;
        this.displayContent();
    }

    displayContent() {
        if (this.content.length <= this.maxLength) {
            this.currentContent = this.content;
            this.isCollapsed = false;
            return;
        }

        if (this.isCollapsed == true) {
            this.currentContent = this.content.substring(0, this.maxLength) + "...";
        } else if(this.isCollapsed == false) {
            this.currentContent = this.content;
        }
    }

    ngOnChanges() {
        if(!this.checkSource(this.content)) {
            console.error('Source must be a string.');
        } else {
            this.displayContent();
        }
    }

    checkSource(s) {
        if(typeof s !== 'string') {
            return false;
        } else {
            return true;
        }
    }
}

Usage example:

<read-more [content]="'This is a test content'" [maxLength]="10" [displayToggleButton]="true"></read-more>

Answer №5

Once again, I successfully tackled these types of problems using dynamic data and maintaining full control.

<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> 

Within personalBasicModel.professionalSummary, there is a string resembling any text.
slice:0:200 = utilizing the slice pipe to truncate the string to 200 characters. You can adjust this length based on your needs. id="dots" & id="more" are two crucial elements.

<div class="Basic-Info-SeeMore">
            <button class="SeeMore"(click)="showMore(passValueOn_SeeMoreBtn)">
                {{showLess_More}}
            </button>
        </div>

Here, we have set up a button with dynamic text (see more and see less ) that triggers a click event.

//---------------------------------- ts file -----------------------------------//

Initialization of variables:

showLess_More : string = "SEE MORE...";
passValueOn_SeeMoreBtn : boolean = true;

Event(Method) executed upon clicking the see more button:

 showMore(data:boolean){
    if(data){
      $("#dots").css('display', 'none');
      $("#more").css('display', 'inline');
      this.showLess_More = "SEE LESS ...";
      this.passValueOn_SeeMoreBtn = false;
    }else{
      $("#dots").css('display', 'inline');
      $("#more").css('display', 'none');
      this.showLess_More = "SEE MORE...";
      this.passValueOn_SeeMoreBtn = true;

    }

  }

Answer №6

For those looking to ensure that text is displayed in its entirety without cutting off any words, make the following adjustment to this section of code:

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 + "..."

Answer №7

Just a slight modification to @Andrei Zhytkevich's code snippet (great 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>

Answer №8

Appreciate the feedback, I made some adjustments to ensure it functions correctly with NgOnInit and resolves the console error. There have been slight modifications to enhance compatibility 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 {

// text content for the container
@Input() text: string;

// maximum height of the container
@Input() maxHeight: number;

// set these to false to determine the expanded container's height 
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;
  }
 }
}

The following adjustment has been made:

const current Height = document.getElementById('textCollapse').offsetHeight;

Answer №9

This plugin is a great solution.

All you need to do is provide the parameters [text] and [textLength] to display your content by default. Check out this helpful plugin here

Answer №10

Implementing Lineheight Technique:

By utilizing the lineheight property along with a bit of calculation and applying certain CSS styles like text-overflow: ellipsis;, achieving the desired result becomes feasible.

.css

.descLess {
  margin-bottom: 10px;
  text-overflow: ellipsis;
  overflow: hidden;
  word-wrap: break-word;
  display: -webkit-box;
  line-height: 1.8;      <==== adjust line-height as per requirement
  letter-spacing: normal;
  white-space: normal;
  max-height: 52px;  <==== set to 250px or other values accordingly
  width: 100%;
  /* autoprefixer: ignore next */
  -webkit-line-clamp: 2; <==== clamp lines at 2, 3, 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;"> 
         <!--Utilize 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 Ipsum 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();
            }

        }

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular has surpassed the maximum call stack size, resulting in a Range Error

I am facing an issue while trying to include machine detail and a button bar in my app. Interestingly, this setup has worked perfectly fine in other parts of the application but is causing errors in the core module. Here is the error message main.ts impo ...

Exploring the way to reach a specific page in Ionic3

Is there a way to navigate from a child page back to its parent page without the URL getting messed up? It seems like when I try to do this, the child remains unchanged while only the parent page updates. If this is correct, how can I remove the child fr ...

Ways to set the initial value of an input[range] in Angular2 when the value exceeds 100

After encountering a similar issue with AngularJS, I posted a question on StackOverflow titled How to initialize the value of an input[range] using AngularJS when value is over 100. As I dive into learning Angular2, I am curious if it handles initializatio ...

When a module with routes is imported into another module with lazy-loading, the routing system becomes disrupted

Within our angular application running version 4.3.6, we have implemented lazy-loaded modules such as Fleet, Maintenance, and Car. Examining our primary app router structure: const routes: Routes = [ { path: '', redirectTo: 'fleet', ...

What is the proper way to declare and utilize a constant list within a component template in NuxtJs?

Can someone help me with using itemList in a template? The itemlist is a static list, but I am unsure of where to declare it and how to export it to the template. <template> <table class="table table is-striped is-narrow is-fullwidth" ...

Error encountered in Angular 7.2.0: Attempting to assign a value of type 'string' to a variable of type 'RunGuardsAndResolvers' is not allowed

Encountering an issue with Angular compiler-cli v.7.2.0: Error message: Types of property 'runGuardsAndResolvers' are incompatible. Type 'string' is not assignable to type 'RunGuardsAndResolvers' This error occurs when try ...

What is the best way to run ng test for all components within a sub module of an Angular application?

In my Angular application, I have defined the app module as follows: app.module.ts export class AppModule {} I am able to execute tests using ng test MyApp Within this application, I also have multiple modules set up like so: my-one.module.ts / my-oth ...

Guide to creating an Express + TypeScript API using an OpenAPI 3.0 specification

After creating specifications for my REST API server using OpenAPI 3.0, I found myself wanting to generate an expressjs app quickly instead of manually writing repetitive code. However, the generated code from editor.swagger.io is in javascript, which does ...

Receiving undefined when subscribing data to an observable in Angular

Currently, I am facing an issue in my Angular project where subscribing the data to an observable is returning undefined. I have a service method in place that retrieves data from an HTTP request. public fetchData(): Observable<Data[]> { const url = ...

Develop a customized interface for exporting styled components

I am struggling to figure out how to export an interface that includes both the built-in Styled Components props (such as as) and my custom properties. Scenario I have created a styled component named CustomTypography which allows for adding typographic s ...

Guide to establishing intricate conditions for TypeORM insertion

When attempting to insert data based on a specific condition, such as if shopId = "shopA", I want to include the shopdetail. In order to achieve this, I have implemented the following business logic, which is somewhat complex. Is there a more ef ...

Unable to find the locally stored directory in the device's file system using Nativescript file-system

While working on creating an audio file, everything seems to be running smoothly as the recording indicator shows no errors. However, once the app generates the directory, I am unable to locate it in the local storage. The code I am using is: var audioFo ...

Printing content from an Angular dashboard can be achieved by following a few

Currently, I am working on developing a legal document automation program for my company. However, I have encountered an issue during the final stages of completing this web application. For the layout and setup, I am using the default angular Dashboard l ...

Retrieve input from text field and showcase in angular 6 with material design components

Take a look at the output image . In the code below, I am displaying the contents of the messages array. How can I achieve the same functionality with a text box and button in an Angular environment? <mat-card class="example-card"> <mat-car ...

Multiple occurrences of Angular @HostListener event being triggered

I am currently working on an Ionic 3 application where I have created an "auto-complete" directive. This directive triggers an auto-complete dialog when the element is focused. The implementation of this in the directive looks like this: @HostListener(&ap ...

Caution in NEXTJS: Make sure the server HTML includes a corresponding <div> within a <div> tag

Struggling with a warning while rendering pages in my Next.js and MUI project. Here's the code, any insights on how to resolve this would be greatly appreciated! import "../styles/globals.scss"; import { AppProps } from "next/app"; ...

Testing the timeout of Angular Karma tests while evaluating the AppComponent with the CUSTOM_ELEMENTS_SCHEMA

While integrating an app-component test into my Angular project, I encountered a timeout issue when running all the tests: [launcher]: Launching browsers headless with concurrency unlimited 21% building 95/96 modules 1 active .../src/css/public.scss19 [l ...

What is the best approach for promoting reusability in Angular: creating a new CSS class or a new component?

I have a div with a set of CSS properties that are essential to my application. These properties will be reused across multiple pages and components. <div style="display: flex; flex-direction: column; height: 100%"> //inner html will vary based on c ...

Tips for monitoring changes to files while developing a NestJs application within a Docker container

Having an issue with NestJS and Docker here. Trying to run the development script using npm start: dev, but encountering a problem where the app runs fine but doesn't detect any changes in the source files, hindering the development process. Here&apo ...

How can I dynamically set the selected index in Nebular's nb-stepper using code?

Currently, I am testing out the nb-stepper component and wanted to configure the selectedIndex in my Angular component. However, when I try to apply two-way binding on [(selectedIndex)] and refresh my activities, the nb-step does not update to display the ...