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

Encountering a TypeError in Angular FormControl Jest test: Circular structure being converted to JSON issue

I encountered an error while running tests using Jest in my Angular project UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property &apos ...

Angular 2 components not properly handling two-way binding errors

Exploring how to achieve two-way binding in Angular 2, I am currently working with the following parent component setup: app.component.html: <child [(text)]="childText" (textChanged)="textChanged($event)"></child> <span>{{childText}}< ...

Is it possible to create an observable with RXJS that emits only when the number of items currently emitted by the source observables matches?

I am dealing with two observables, obs1 and obs2, that continuously emit items without completing. I anticipate that both of them will emit the same number of items over time, but I cannot predict which one will emit first. I am in need of an observable th ...

A step-by-step guide for updating a minor version of Angular with Angular CLI

I've been searching online for the answer to this straightforward question, but can't seem to find it anywhere... In my angular 4 project (made with angular cli), I want to utilize the newly introduced http interceptors in version 4.3. Could so ...

Navigating back to the login page in your Ionic V2 application can be achieved by utilizing the `this.nav

Running into an issue with navigating back to the login screen using Ionic V2. Started with the V2 tabs template but added a custom login page, setting rootPage = LoginPage; in app.components.ts. When the login promise is successful, I used this.nav.setR ...

What is the best way to programmatically generate a service within Angular?

Is there a way to dynamically create services at runtime using a string name (like reflection)? For example: let myService = new window[myClassName](myParams); Alternatively, you could try: let myService = Object.create(window[myClassName].prototype); m ...

Navigating to the Login page in Angular 13

<app-navbar></app-navbar> <div class = "app-body"> <div class="app-sidebar"> <app-sidebar></app-sidebar> </div> <div class="app-feed"> <router-outlet name="main& ...

The file node_modules/angular2-qrscanner/angular2-qrscanner.d.ts has been detected as version 4, while version 3 was expected. Resolving symbol

We're encountering a Metadata error that is causing obstacles in our deployment process. This issue is preventing the execution of ng build. Below, you will find the configuration details along with the complete error trace. ERROR in Error: Metadata ...

Guide to dynamically resizing the Monaco editor component using react-monaco-editor

Currently, I am integrating the react-monaco-editor library into a react application for viewing documents. The code snippet below showcases how I have set specific dimensions for height and width: import MonacoEditor from 'react-monaco-editor'; ...

Subscribing to valueChanges in reactive forms to dynamically update checkbox options

My goal is to create a select dropdown with options for bmw, audi, and opel. The user can only select one option from the dropdown, but they should also have the ability to select the options that were not chosen using checkboxes. cars = [ { id: 1, na ...

An issue is preventing the Angular 2+ http service from making the requested call to the server

I am looking to create a model class that can access services in Angular. Let's say I have the following endpoints: /book/:id /book/:id/author I want to use a service called BooksService to retrieve a list of Book instances. These instances should ...

Issue with displaying decimal places in Nivo HeatMap

While utilizing Nivo HeatMap, I have observed that the y value always requires a number. Even if I attempt to include decimal places (.00), it will still trim the trailing zeros and display the value without them. The expected format of the data is as foll ...

Enhancing an existing Angular1 project to Angular4 while incorporating CSS, Bootstrap, and HTML enhancements

Facing CSS Issues in AngularJs to Angular4 MigrationCurrently in the process of upgrading an AngularJS project to Angular 4, specifically focusing on updating views. However, encountering issues with certain views not displaying the custom CSS from the the ...

Converting JSON data types into TypeScript interface data types

Struggling to convert data types to numbers using JSON.parse and the Reviver function. I've experimented with different options and examples, but can't seem to figure out where I'm going wrong. The Typescript interface I'm working with ...

Guide to accessing Angular app on a mobile device within the same network

I'm facing an issue with my Angular App when trying to access it on mobile within the same network. I've attempted running ng serve --host <my IP> or ng serve --host 0.0.0.0 and it works well. However, the problem arises because the applica ...

Can you explain the significance of the syntax #foo="myFoo" used in this template?

I grasp the concept of the hashtag syntax <input #myinput > which gives a name for element access, but I'm puzzled by the similar syntax used in an example on the angular material website: <mat-menu #menu="matMenu"> What does t ...

Utilizing Angular2 Observables for Time Interval Tracking

I'm working on a function that needs to be triggered every 500ms. My current approach in angular2 involves using intervals and observables. Here's the code snippet I've implemented so far: counter() { return Observable.create(observer =&g ...

The proper order for logging in is to first validate the login credentials before attempting

I created a custom validation class to verify if a user is logged in before allowing them access to a specific page. However, after implementing this validation, my program no longer routes me to the intended component. Validation.ts export class UserVal ...

Node.js/Express API Endpoint Ceases Functioning

In my Angular/Express.js app, there is a post method within my api.service.ts file: post(data: any, endpointUrl: string): Observable<T> { console.log("REACHED POST METHOD") return this.http.post<T>(`${this.apiUrl}/${endpoint ...

Best Practices for Organizing Imports in Typescript to Prevent Declaration Conflicts

When working with TypeScript, errors will be properly triggered if trying to execute the following: import * as path from "path" let path = path.join("a", "b", "c") The reason for this error is that it causes a conflict with the local declaration of &ap ...