How to show "show more" button when text has ellipsis? - javascript

I have searched on Google and here on SO before posting this question.
I have found several solution to my problem, but none of them fits my needs.
Here is my code: Plunker
<div *ngIf="description.length > 200" class="ui mini compact buttons expand">
<button class="ui button" (click)="showMore($event)">Show more</button>
</div>
The "show more" button appears only if text length exceeds 200 characters.
As you can see it seems to be a nice solution.
showMore(event: any) {
$(event.target).text((i, text) => { return text === "Show more" ? "Show less" : "Show more"; });
$(event.target).parent().prev().find('.detail-value').toggleClass('text-ellipsis');
}
Anyway I could have a text that is not 200 characters long and that doesn't fit the SPAN element, then it has the ellipsis but the "show more" button doesn't appear.
How can I make my solution work in any case? Do you know a workaround or a best solution to solve that?

Edit with a possible solution:
//our root app component
import {Component, NgModule, VERSION, OnInit} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import {ElementRef,ViewChild} from '#angular/core';
#Component({
selector: 'my-app',
template: `
<div class="ui segment detail-container" (window:resize)="checkOverflow(span)">
<span class="title-container" role="heading">User details</span>
<div class="detail-group">
<div class="detail-element">
<span class="detail-label">Name</span>
<span class="detail-value">John</span>
</div>
<div class="detail-element">
<span class="detail-label">Surname</span>
<span class="detail-value">Smith</span>
</div>
</div>
<div class="detail-group">
<div class="detail-element">
<span class="detail-label">Description</span>
<span #span class="detail-value text-ellipsis">{{description}}</span>
</div>
<div class="ui mini compact buttons expand">
<button *ngIf="checkOverflow(span) && showMoreFlag" class="ui button" (click)="showMore($event)">Show more</button>
<button *ngIf="!showMoreFlag" class="ui button" (click)="showMore($event)">Show less</button>
</div>
</div>
</div>
`,
styleUrls: ['src/app.css']
})
export class App implements OnInit {
description: string = 'Lorem ipsum dolor sit a ';
showMoreFlag:boolean = true;
constructor() {
}
ngOnInit(): void {
this.overflowOcurs = this.checkOverflow(this.el.nativeElement);
}
showMore(event: any) {
this.showMoreFlag = !this.showMoreFlag;
$(event.target).parent().prev().find('.detail-value').toggleClass('text-ellipsis');
}
checkOverflow (element) {
if (element.offsetHeight < element.scrollHeight ||
element.offsetWidth < element.scrollWidth) {
return true;
} else {
return false;
}
}
}
#NgModule({
imports: [ BrowserModule ],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
Plunker working properly:
https://plnkr.co/edit/HCd6ds5RBYvlcmUtdvKr

I Recommend using the "ng2-truncate".
With this component, you can truncate your codes with length or word count or something else.
I hope this component help you.
Plunker
npm

Related

Angular: How to mark a validator as dirty manually

I have a component I would like to mark as dirty when the "Next" button in my stepper component is clicked.
Currently inside my stepper.component.ts I have a function displayValidation, which is called when my Next button is clicked and manually marks all form inputs as touched (I have also double checked each's status to make sure touched == true when the Next button is clicked):
displayValidation(field: FormlyFieldConfig) {
if (field?.fieldGroup){
field.fieldGroup.forEach((curFieldGroup) => {
curFieldGroup.formControl.markAsTouched();
this.setFieldTypeInvalid(curFieldGroup.type, curFieldGroup.formControl.status);
});
}
}
This successfully changes all my components to touched, however for number input component, it does nothing visually (expecting a red outline/dirty) despite touched now equalling true, and validation existing for the component (called currency component).
Here is my currency.component.ts in question:
import { Component } from '#angular/core';
import { CurrencyPipe} from '#angular/common';
import { FieldType } from '#ngx-formly/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
#Component({
selector: 'app-formly-field-currency',
templateUrl: './currency.component.html'
})
export class FormlyFieldCurrencyComponent extends FieldType {
constructor(private currencyPipe: CurrencyPipe) {
super();
}
currencyGroup = new FormGroup({
currencyForm: new FormControl('', [
Validators.required
])
});
get currencyAmount(){
return this.currencyGroup.get('currencyForm');
}
}
In my corresponding currency.component.html, I have formGroup and formControl validation that are fired when the currency component is clicked manually, but I would like it to fire in the above stepper.component.ts such that it is visually dirty when the Next button is clicked:
currency.component.html:
<div class="input-group mb-3">
<form [formGroup]="currencyGroup">
<div class="input-group mt-2 flex-nowrap">
<div class="input-group-prepend">
<span class="input-group-text">$</span>
</div>
<input matInput
[class.is-invalid] = "currencyGroup.get('currencyForm').invalid && currencyGroup.get('currencyForm').touched"
type="number"
class="form-control"
[formControl]="formControl"
formControlName="currencyForm" #input
[formlyAttributes]="field"
>
</div>
<div
class="mx-auto"
*ngIf="(currencyAmount.invalid && currencyAmount.touched) || currencyAmount.dirty">
<small *ngIf="currencyAmount.errors?.required" class="text-danger">
Currency amount is required.
</small>
</div>
</form>
</div>
I understand this is a lot here, however I feel as if I am so close, so any help would be greatly appreciated.
Thanks!

Why Parent does not listen to child on Angular?

I cannot make this eventemitter work. Can you please help? I am a beginner and it should be quite simple code for you.
I have a parent component, reading two different emitters from two different children:
<app-van [vans]="vans"></app-van>
<app-modal *ngIf="modalOpen" (closed)="onClick()" (openModal)="onClickTwo($event)"></app-modal>
import { Component, OnInit } from '#angular/core';
import { Van } from '../../interface';
#Component({
selector: 'app-fleet-home',
templateUrl: './fleet-home.component.html',
styleUrls: ['./fleet-home.component.css']
})
export class FleetHomeComponent implements OnInit {
modalOpen = true;
vans: Van [] = [
{ name: 'Ubeddu', description: 'Mercedes Sprinter', plate: 'NH55GKA' },
{ name: 'Abbestia', description: 'Ford Transit', plate: 'DK66HHR' },
{ name: 'Eumulu', description: 'Citroen Berlingo', plate: 'DR55MKL' }
];
constructor( ) { }
ngOnInit() {
}
onClick() {
this.modalOpen = !this.modalOpen;
console.log('modalOpen changed');
}
onClickTwo(event) {
this.modalOpen = event;
console.log('modalOpen changed');
}
}
the parent listened to this child:
<div (click)="onCloseClick()" class="ui dimmer visible active">
<div (click)="$event.stopPropagation()" class="ui modal visible active">
<div class="asuca">
<form class="ui form" >
<h4 class="ui dividing huge header">Van</h4>
<div class="required field">
<label class="ui header">Van Name</label>
<input type="text" placeholder="Van NickName">
</div>
<div class="field">
<label class="ui header">Description</label>
<input type="text"placeholder="Description">
</div>
<div class="field">
<label class="ui header">Plate</label>
<input type="text"placeholder="License Plate">
</div>
<button (click)="onCloseClick()" class="ui button" type="submit">Submit</button>
</form>
</div>
</div>
</div>
import { Component, OnInit, ElementRef, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.css']
})
export class ModalComponent implements OnInit {
#Output() closed = new EventEmitter();
constructor(private el: ElementRef) { }
ngOnInit() {
document.body.appendChild(this.el.nativeElement);
}
// tslint:disable-next-line: use-lifecycle-interface
ngOnDestroy() {
this.el.nativeElement.remove();
}
onCloseClick() {
this.closed.emit();
}
}
and doesnt listen to the second child:
<div class="ui fluid four black cards">
<div *ngFor="let van of vans" class="card">
<div class="content">
<div class="header">
{{ van.name }}
</div>
<div class="meta">
{{ van.description }}
</div>
<div class="description">
{{ van.plate }}
</div>
</div>
<div class="extra content">
<div class="ui two buttons">
<div (click)="onEditClick(true)" class="ui basic black button">Edit</div>
<div class="ui basic red button">Delete</div>
</div>
</div>
</div>
</div>
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-van',
templateUrl: './van.component.html',
styleUrls: ['./van.component.css']
})
export class VanComponent implements OnInit {
#Input() vans = [];
#Output() openModal = new EventEmitter<boolean>();
constructor() { }
ngOnInit() {
}
onEditClick(event: boolean) {
this.openModal.emit(event);
}
}
the whole thing is to hide the modal clicking around the screen and show it again clicking a button.
On the console.log, the object emitter by the first child has got an observer, where the object emitter by the second child as none; no idea what means though.
thanks in advance for the help. I can provide the whole folder if needed. I am just trying to learn :)
Seems like you have missed binding the output event in parent template. Please correct like below:
<app-van [vans]="vans" (openModal)="onEditClick($event)"></app-van>

Angular 8: detect if a ng-content has content in it (or exists)

I have a component whose template allows for 2 content areas: Text and "read more" text. If the consumer of the component adds the area for the "read more" text, I want to show the "read more" link the end-user would click to show the text. If they don't include/need any "read more" text I don't want to show the link.
How do I detect the presence of the template area, and act accordingly with an ngIf?
For example, the html might be:
<app-promohero-message-unit title="Title for messaging module">
<div description>
Include a short, informative description here.
</div>
<div readmoretext>
If you need to add more detail, include another sentence or two it in this section.
</div>
</app-promohero-message-unit>
Obviously, they might not need readmoretext, so if they've omitted it I should not show the readmore link.
The component code is, so far:
import { Component, Input } from '#angular/core';
#Component({
selector: 'app-promohero-message-unit',
template: `
<div>
<h3 class="text-white">{{ title }}</h3>
<p class="text-white">
<ng-content select="[description]"></ng-content>
</p>
<p class="text-white" *ngIf="readMore">
<ng-content select="[readmoretext]"></ng-content>
</p>
</div>
<p>
<a class="text-white" (click)="showReadMore()" *ngIf="something"><u>Read more</u></a>
</p>
`
})
export class PromoheroMessageUnitComponent {
#Input()
title: string;
readMore = false;
showReadMore() {
this.readMore = true;
}
}
In Angular 8 you dont have to use the ngAfterViewInit life cycle hook. You can use the ngOnInit as long as you set the "static" value of the viewchild to true.
import { Component, OnInit, ViewChild, TemplateRef, ElementRef } from '#angular/core';
#Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
#ViewChild('content', { read: ElementRef, static: true }) content: ElementRef;
constructor() { }
ngOnInit() {
console.log(!!this.content.nativeElement.innerHTML); // return true if there is a content
}
}
Note that you must wrap the ng-content directive with html tag (such as div, span etc) and to set the templateRef on this outer tag.
<div #content>
<ng-content></ng-content>
</div>
I putted it on stackblitz: https://stackblitz.com/edit/angular-8-communicating-between-components-mzneaa?file=app/app.component.html
You can get a reference to the ng-content (Template Variable) and then access that variable in your component to check the length on the content of that ng-content using ViewChild
Then you can use the ngAfterViewInit life cycle hook to check for ng-content length
Your code will be like this:
import { Component, Input, ViewChild, ElementRef } from '#angular/core';
#Component({
selector: 'app-promohero-message-unit',
template: `
<div>
<h3 class="text-white">{{ title }}</h3>
<p class="text-white">
<ng-content select="[description]"></ng-content>
</p>
<p class="text-white" *ngIf="readMore">
<ng-content #readMoreContent select="[readmoretext]"></ng-content>
</p>
</div>
<p>
<a class="text-white" (click)="showReadMore()" *ngIf="something"><u>Read more</u></a>
</p>
`
})
export class PromoheroMessageUnitComponent {
#Input()
title: string;
#ViewChild('readMoreContent') readMoreContent: ElementRef;
readMore = false;
ngAfterViewInit() {
if (this.readMoreContent.nativeElement.childNodes.length.value == 0){
this.readMore = false
}
}
showReadMore() {
this.readMore = true;
}
}
You can use the ContentChild decorator for this, but will need to use an ng-template with a defined id as your content:
<app-promohero-message-unit title="Title for messaging module">
<div description>
Include a short, informative description here.
</div>
<ng-template #readmoretext>
If you need to add more detail, include another sentence or two it in this section.
</ng-template>
</app-promohero-message-unit>
Then in your component, you can use the ContentChild annotation like this:
export class PromoheroMessageUnitComponent {
#ContentChild('readmoretext')
readMoreContent: TemplateRef<any>;
// ...snip
}
Then finally in the HTML for your component:
<!-- snip -->
<p class="text-white" *ngIf="readMoreContent">
<ng-container *ngTemplateOutlet="readMoreContent"></ng-container>
</p>

classList.toggle() not working IE11 Angular 7 (Invalid Calling Object)

I've just been testing my app on IE11 and I cant figure out why this isn't working,
I have this code it has three elements .hamburger-small, .hamburger-big and .menu
<div [class.shown]="!chatbarFullscreen">
<div [class.disabled]="router.url.includes('home')">
<img (click)="closeChatbar(true, router.url.includes('home') ? true : false)" *ngIf="chatbarFullscreen" src="../assets/images/whole-app/arrow-right.svg" alt="Arrow Right">
<img (click)="closeChatbar(false, router.url.includes('home') ? true : false)" *ngIf="!chatbarFullscreen" src="../assets/images/whole-app/arrow-left.svg" alt="Arrow Left">
</div>
<img (click)="goHome()" src="../assets/images/chatbar/header-logo.svg" alt="header logo">
<div id="small" (click)="hamburgerClick()" class="hamburger hamburger--slider hamburger-small">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
</div>
<div id="big" (click)="hamburgerClick()" class="hamburger hamburger--slider hamburger-big">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
<div class="menu">
<p (click)="closeChatbar(false); hamburgerClick();" [routerLink]="['/app/main/home']">Home</p>
</div>
</div>
and when you click it, it calls this function
hamburgerClick() {
const small = <HTMLElement>document.querySelector('.hamburger-small');
const big = <HTMLElement>document.querySelector('.hamburger-big');
const menu = <HTMLElement>document.querySelector('.menu');
small.classList.toggle('is-active');
big.classList.toggle('is-active');
menu.classList.toggle('show');
}
now It works on every other browser, Chrome, Firefox, Safari and Edge but not in IE I've seen similar questions but it seems as if it should work? I'm also getting this error in the console when I click the button for the first time, but it does not happen any other time
any help would be great..
EDIT
I have tried using #ViewChild() but it still isn't working, however the Invalid Calling Object error is no longer happening
#ViewChild('hamburgerBig') hamburgerBig: ElementRef;
#ViewChild('hamburgerSmall') hamburgerSmall: ElementRef;
#ViewChild('menu') menu: ElementRef;
hamburgerClick() {
this.hamburgerBig.nativeElement.classList.toggle('is-active');
this.hamburgerSmall.nativeElement.classList.toggle('is-active');
this.menu.nativeElement.classList.toggle('show');
}
Thanks!!
try using Renderer2 to manipulate dom elements along with ElementRef and ViewChild as other previously mentioned.
first import ViewChild, ElementRef and Renderer2
import { Renderer2, ElementRef, ViewChild } from '#angular/core';
get the Element using ViewChild of type ElementRef after you've made template references in your DOM, like
<div #hamburgerBig></div>
<div #hamburgerSmall></div>
<div #menu></div>
#ViewChild('hamburgerBig') hamburgerBig: ElementRef;
#ViewChild('hamburgerSmall') hamburgerSmall: ElementRef;
#ViewChild('menu') menu: ElementRef;
and do your stuff with your hamburgerClick function
hamburgerClick() {
const hamBigIsActive = this.hamburgerBig.nativeElement.classList.contains('is-active');
const hamSmallIsActive = this.hamburgerSmall.nativeElement.classList.contains('is-active');
const menuShow = this.menu.nativeElement.classList.contains('show');
if(hamBigIsActive) {
this.renderer.removeClass(this.hamburgerBig.nativeElement, 'is-active');
} else {
this.renderer.addClass(this.hamburgerBig.nativeElement, 'is-active');
}
if(hamSmallIsActive) {
this.renderer.removeClass(this.hamburgerSmall.nativeElement, 'is-active');
} else {
this.renderer.addClass(this.hamburgerSmall.nativeElement, 'is-active');
}
if(hamSmallIsActive) {
this.renderer.removeClass(this.menu.nativeElement, 'show');
} else {
this.renderer.addClass(this.menu.nativeElement, 'show');
}
}
or you could just simply use [ngClass](not sure why you aren't using this instead)
hope this helps
also dont forget to add render to your contructor
contructor(private renderer: Renderer2){}
Edit: here's the [ngClass] implementation
<div id="small"
(click)="hamburgerClick()"
[ngClass] = "{'is-active' : hamClick}"
class="hamburger hamburger--
slider hamburger-small">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
<div id="big"
(click)="hamburgerClick()"
[ngClass] = "{'is-active' : hamClick}"
class="hamburger hamburger--slider
hamburger-big">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
<div
[ngClass] = "{'show' : hamClick}"
class="menu">
<p (click)="closeChatbar(false); hamburgerClick();" [routerLink]="
['/app/main/home']">Home</p>
</div>
and then just use a function to switch
hamClick: boolean
hamburgerClick(){
this.hamClick = !this.hamClick;
}
there you go
Try to make a test with code below may help you to solve your issue.
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public show:boolean = false;
public buttonName:any = 'Show';
ngOnInit () { }
toggle() {
this.show = !this.show;
// CHANGE THE NAME OF THE BUTTON.
if(this.show)
this.buttonName = "Hide";
else
this.buttonName = "Show";
}
}
.is-active{color:green;
}
<button (click)="toggle()" id="bt">
Hide
</button>
<ng-container *ngIf="show">
<div style="margin: 0 auto;text-align: left;">
<div>
<label>Name:</label>
<div><input id="tbname" name="yourname" /></div>
</div>
<div>
<label>Email Address:</label>
<div><input name="email" id="email" /></div></div>
<div>
<label>Additional Information (optional):</label>
<div><textarea rows="5" cols="46"></textarea></div>
</div>
</div>
</ng-container>
Further, You can try to modify the code based on your requirement.

Angular: Hiding/showing elements by ngIf or Toggling a class?

I'm wondering what the best way to approach this problem is and I'm very new to TypeScript and Angular. Using Angular 5.
Anyway. I have a list of elements on a page via a table.
This is the code that controls said list.
<tbody>
<tr class="text-center" *ngFor="let topic of topics">
<td *ngIf="!editTopicMode">{{ topic.name }}</td>
<td id="{{topic.id}}" *ngIf="editTopicMode">
<form>
<div class="form-group">
<input class="form-control" type="text" name="name" value="{{topic.name}}" />
</div>
</form>
</td>
<td>
<div *ngIf="!editTopicMode" class="btn-group btn-group-sm">
<button class="btn btn-link" (click)="editTopicBtnClick(topic.id)">
<i class="fa fa-pencil fa-2x" aria-hidden="true"></i>
</button>
<button class="btn btn-link">
<i class="fa fa-trash fa-2x" aria-hidden="true"></i>
</button>
</div>
<div *ngIf="editTopicMode" class="btn-group-sm">
<button class="ml-2 btn btn-sm btn-outline-secondary" (click)="cancelEditMode()">Cancel</button>
<button class="ml-2 btn btn-sm btn-outline-primary">Save</button>
</div>
</td>
</tr>
</tbody>
What I'm aiming to do is that if a user clicks on the pencil(edit) icon, then the adjacent div changes from just a regular td to an input and the edit/delete buttons to change to a cancelEdit/save edits button group. (I know that I need to change the html a bit because the buttons aren't in the form element currently, but I'm not there on the wiring it up part).
I've thought of two ways to do this. 1) with ngIf's so that I can conserve the elements that are rendered and the edit/cancel buttons toggle the editMode; or 2) use ngClass and toggle display:none css classes for the button clicked.
Right now, when you click the edit button, regardless of which edit button you click, it flips all the columns to inputs, rather than just the row the user wants to edit.
Here's my component ts:
import { Component, OnInit, TemplateRef, ElementRef, ViewChild, Inject } from '#angular/core';
import { Topic } from '../models/topic';
import { TopicService } from '../services/topicService/topics.service';
import { AlertifyService } from '../services/alertify/alertify.service';
import { ActivatedRoute } from '#angular/router';
import { DOCUMENT } from '#angular/common';
#Component({
selector: 'app-topics',
templateUrl: './topics.component.html',
styleUrls: ['./topics.component.css']
})
export class TopicComponent implements OnInit {
#ViewChild('topicId') topicId: ElementRef;
topics: Topic[];
newTopic: Topic = {
id: 0,
name: '',
};
editTopicMode = false;
constructor(
#Inject(DOCUMENT) document,
private topicsService: TopicService,
private alertify: AlertifyService,
private route: ActivatedRoute
) { }
ngOnInit() {
//this.route.data.subscribe(data => {
// this.topics = data['topics'];
//})
this.getTopics();
}
getTopics() {
this.topicsService.getAllTopics()
.subscribe(data => {
this.topics = data;
}, error => {
this.alertify.error(error);
});
}
addTopic() {
this.topicsService.createTopic(this.newTopic)
.subscribe((topic: Topic) => {
this.topics.push(topic);
this.alertify.success(this.newTopic.name + ' added as a new topic.');
this.newTopic.name = '';
},
(err: any) => {
this.alertify.error(err);
}
)
}
editTopicBtnClick(event) {
console.log(event);
this.editTopicMode = true;
console.log(document.getElementById(event));
}
cancelEditMode() {
this.editTopicMode = !this.editTopicMode;
}
}
Any thoughts on the best (most efficient) way to make this happen?
You've done all the hard work already.
For single item editing, all that's left is: change editTopicMode to something like editTopicId.
Then you can:
Set it to topic.id on edit enabled, and null for example on edit closed
Change your *ngIf to editTopicId === topic.id (or !== as needed)
And that should be all.
If you want to enable multiple editing, just add a property called isInEditMode to each topic.
Your *ngIf check becomes topic.isInEditMode
No isInEditMode property at all is just like false, because undefined is a falsy value
Set topic.isInEditMode to true on editing enabled, false on editing closed
Using *ngIf is fine just make sure that you set the *ngIf variable to point to somethign specific to the particular row of the *ngFor
this example could be cleaner but you could accomplish it as simply as
<button (click)="topic.edit = true" *ngIf="topic.edit === false">edit</button>
<button (click)="topic.edit = false" *ngIf="topic.edit === true">cancel</button>

Categories

Resources