I am new to angular and I have a p tag with a list of paragraphs and data(nested array of objects) for p tag will be coming from the backend. I need to truncate the text after some character limit and show....show more. when a user clicks on the p tag it should show reveal the rest of the text. I have figure out the way to truncate the text and display ...show more.clicking should reveal that specific paragraph text but in my case, all other paragraphs texts which are truncated are also showing full texts and since the data is nested array of objects it is tricky for me and i am not able to figure out the solution. I am providing the stackblitz link below. any help will be appreciated.
stackblitz link
data = [
{
comments:[
{text:'this is comment',id:'1'},
{text:'this is comment',id:'2'}
]
},
{
comments:[
{text:'this is comment',id:'3'},
{text:'this is comment',id:'4'}
]
},
{
comments:[
{text:'this is comment',id:'5'},
{text:'this is comment',id:'6'}
]
}
]
showrest:boolean = false
<div *ngFor="let c of data">
<div *ngFor="let comment of c['comments']">
<p (click)="showrest=true">{{showrest?comment.text:(comment.text | slice:0:10)+'...Click to Read More'}}</p>
</div>
</div>
Here is how you could do, see this repro on stackblitz. You need a context for each comment so just create a component for it. Here is the code :
ts:
import { Component, Input } from "#angular/core";
#Component({
selector: "app-comment",
templateUrl: "./comment.component.html",
styleUrls: ["./comment.component.css"]
})
export class CommentComponent {
#Input() comment;
showrest = false;
}
html:
<p (click)="showrest=!showrest">{{showrest?comment.text:(comment.text | slice:0:10)+'...Click to Read More'}}</p>
and you call it as follow :
app.html
<div *ngFor="let c of data">
<div *ngFor="let comment of c['comments']">
<app-comment [comment]="comment"></app-comment>
</div>
</div>
Your problem was that each of your comment were bound to the same variable (Of the same component), so when yu click on a paragraph it update your variable and therefore all the binded element (here your paragraphs).
Another solution could be to add a property to your comment model, something like :
{text:'this is comment',id:'1', isFullyVisible: false},
This way your could handle the display of your comment with this variable. The best solution depends on what you intend to do with your comment. If it's just a display and nothing else then just adding a property to your model is good enough.
Related
I am making angular application with angular form.
Here i have given a form with input fields first name and last name which will always showing..
After that i am having children which will be displayed upon clicking the add button and the children will get removed on click remove button.
As of now everything works fine.
Here i am making patching of data to the inputs on click option from select box.. The neccessary inputs gets patched..
HTML:
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div *ngFor="let question of questions" class="form-row">
<ng-container *ngIf="question.children">
<div [formArrayName]="question.key">
<div *ngFor="let item of form.get(question.key).controls; let i=index" [formGroupName]="i">
<div *ngFor="let item of question.children">
<app-question [question]="item" [form]="form.get(question.key).at(i)"></app-question>
</div>
</div>
<select multiple (change)="changeEvent($event)">
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
</div>
</ng-container>
<ng-container *ngIf="!question.children">
<app-question [question]="question" [form]="form"></app-question>
</ng-container>
</div>
<div class="form-row">
<!-- <button type="submit" [disabled]="!form.valid">Save</button> -->
</div>
</form> <br>
<!-- Need to have add and remove button.. <br><br> -->
<button (click)="addControls('myArray')"> Add </button>
<button (click)="removeControls('myArray')"> Remove </button><br/><br/>
<pre>
{{form?.value|json}}
</pre>
</div>
TS:
changeEvent(e) {
if (e.target.value == 1) {
let personOneChild = [
{ property_name : "Property one" },
{ property_name : "Property two" },
]
for (let i = 0; i < personOneChild.length; i++) {
this.addControls('myArray')
}
this.form.patchValue({
'myArray': personOneChild
});
}
if (e.target.value == 2) {
let personTwoChild = [
{ property_name : "Property three" },
{ property_name : "Property four" },
{ property_name : "Property five" },
]
for (let i = 0; i < personTwoChild.length; i++) {
this.addControls('myArray')
}
this.form.patchValue({
'myArray': personTwoChild
});
}
}
addControls(control: string) {
let question: any = this.questions.find(q => q.key == control);
let children = question ? question.children : null;
if (children)
(this.form.get(control) as FormArray).push(this.qcs.toFormGroup(children))
}
removeControls(control: string) {
let array = this.form.get(control) as FormArray;
array.removeAt(array.length - 1);
}
Clear working stackblitz: https://stackblitz.com/edit/angular-x4a5b6-fnclvf
You can work around in the above link that if you select the person one option then the value named property one and property two gets binded to the inputs and in select box the property one is highlighted as selected..
The thing i am in need is actually from here,
I am having a remove button, you can see in demo.. If i click the remove button, one at last will be got removed and again click the last gets removed..
Here i am having two property one and two, if i remove both the inputs with remove button, the the highlighted value person one in select box needs to get not highlighted.
This is actually my requirement.. If i remove either one property then it should be still in highlighted state.. Whereas completely removing the both properties it should not be highlighted..
Hope you got my point of explanation.. If any needed i am ready to provide.
Note: I use ng-select for it as i am unable implement that library, i am making it with html 5 select box.. In ng-select library it will be like adding and removing the option.. Any solution with ng-select library also appreciable..
Kindly help me to achieve the result please..
Real time i am having in application like this:
Selected three templates and each has one property with one,two,three respectively:
If choose a dropdown then the property values for the respective will get added as children.
Here you can see i have deleted the property name three for which the parent is template three and the template three still shows in select box even though i removed its children
Firstly, get a reference to the select, like so:
HTML:
<select multiple (change)="changeEvent($event)" #mySelect>
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
TS:
import { ViewChild } from '#angular/core';
// ...
#ViewChild('mySelect') select;
Then, in your remove function, check if all elements have been removed, and if they have, set the value of the select to null
if (array.length === 0) {
this.select.nativeElement.value = null;
}
Here is a fork of the StackBlitz
i've built a website i.e. a blog which runs at the moment locally with the full MEAN (MongoDB, Node, Angular and Express) stack. I've already implemented the backend, but have some problems for the frontend. Most of it works. I load the content dynamically i.e. every time the user reached the bottom of the page, i load the next three blog posts. I've also added some voting (up and down). I don't know if i did it right, but it works. I have an array which contains all blog post. Every time the user reached the bottom, i add them via .push to my array. This array is shown via *ngFor, so it contains all current blog post and "future posts". The question arise how i add the voting in an efficient way? I've added an array, every time a user clicks "vote up" i check if the post id is already in that array, if not i add it and increment the vote by one and store the value also in the database by a request. If the user regrets his decision he can anytime press the same button to bring the initial state back, because now the post id is in that array and therefore a click on the same button is not a vote up anymore rather we decrease the vote and remove the post id from the array (and store the new value in the database). The user can now again vote up, because the post id is no more in that array, etc.
Is there any better approach to implement the voting? I think if the user loads 1000 blog articles and votes many articles and afterwards regrets one, then the loading time can eventually very high (finding a value in a big array, etc.).
The point why i've written here is the following. The main problem is that i want to add a commenting box to each blog post. But how do i do that? Should i create each time the browser loads 3 article 3 commenting boxes? "This" is not really a problem, but what happened in the same scenario as already mentioned? What happened if the user reads 20 articles one after the other. Then 20 commenting boxes are created and are binded to some variables, etc. It cant be efficient in that way. I thought i could create one commenting box and every time the user clicks on an anchor tag (named "Reply") the commenting box will be opened. But there exists only one box. It will be shown in the appropriate place. But i don't know how to implement this with angular. :(
I'd appreciate any hint or help. :)
UPDATE 26.11.2017: i share some content from my project, so can probably better understand what i mean.
This is my blog.component.ts
import { Component, OnInit } from '#angular/core';
import { CarouselConfig } from 'ngx-bootstrap/carousel';
import { BlogPost } from '../shared/blogPost';
import { BlogPostFactory } from '../shared/blogPost-factory';
import { DataService } from '../data.service';
#Component({
selector: 'ca-blog',
templateUrl: './blog.component.html',
styleUrls: ['./blog.component.css'],
providers: [{provide: CarouselConfig, useValue: {interval: 1500, noPause: true}}]
})
export class BlogComponent implements OnInit {
private blogPosts: BlogPost[] = [];
private heartsUp: string[] = [];
private likesUp: string[] = [];
constructor(private dataService: DataService) {
window.onscroll = () => {
let windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight;
let body = document.body;
let html = document.documentElement;
let docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
let windowBottom = windowHeight + window.pageYOffset;
if(windowBottom >= docHeight) {
let urlSearchParams = { last: this.blogPosts.length };
this.dataService.getBlogPosts(urlSearchParams).subscribe( result => {
for(let i=0; i<result.length; i++){
this.blogPosts.push(BlogPostFactory.fromObject(result[i]));
}
});
}
};
}
ngOnInit() {
this.dataService.getBlogPosts().subscribe( result => {
for(let i=0; i<result.length; i++){
this.blogPosts.push(BlogPostFactory.fromObject(result[i]));
}
});
}
incrementHearts(index) : void {
let idIndex : number = this.heartsUp.indexOf(this.blogPosts[index].id);
if(idIndex != -1){
this.blogPosts[index].hearts--;
this.heartsUp.splice(idIndex, 1);
} else {
this.blogPosts[index].hearts++;
this.heartsUp.push(this.blogPosts[index].id);
}
this.dataService.editBlogPost(this.blogPosts[index].id, { hearts: this.blogPosts[index].hearts }).subscribe().unsubscribe();
}
incrementLikes(index) : void {
let idIndex : number = this.likesUp.indexOf(this.blogPosts[index].id);
if(idIndex != -1){
this.blogPosts[index].likes--;
this.likesUp.splice(idIndex, 1);
} else {
this.blogPosts[index].likes++;
this.likesUp.push(this.blogPosts[index].id);
}
this.dataService.editBlogPost(this.blogPosts[index].id, { likes: this.blogPosts[index].likes }).subscribe().unsubscribe();
}
}
and blog.component.html is given by
<div class="container">
<div *ngFor="let blogPost of blogPosts; let i=index">
<div *ngIf="i !== 0">
<hr class="my-5">
</div>
<div class="row">
<div class="col">
<article>
<section>
<header style="" class="mb-4">
<h1 style="display:inline" class="m-0 p-0">{{ blogPost.title }}</h1><small style="opacity:0.5" class="d-block d-sm-inline ml-sm-3">zuletzt bearbeitet am {{ blogPost.lastEdited | date }} von <strong>{{ blogPost.author.username }}</strong></small>
<p class="mt-1 mt-sm-auto"><i class="fa fa-tags mr-2"></i><a *ngFor="let hashtag of blogPost.hashtags" href="#" class="badge badge-secondary mr-2">#{{ hashtag }}</a></p>
</header>
<div class="m-0 p-0" [innerHTML]="blogPost.body">
</div>
<div>
<small class="heartUp"><a (click)="incrementHearts(i)" [ngStyle]="{'color':heartsUp.includes(blogPost.id) ? '#E63946' : 'rgba(0,0,0,0.5)'}"><i class="fa fa-heart mr-1"></i>{{ blogPost.hearts }}</a></small>
<small class="likeUp"><a (click)="incrementLikes(i)" [ngStyle]="{'color':likesUp.includes(blogPost.id) ? '#3b5998' : 'rgba(0,0,0,0.5)'}"><i class="fa fa-thumbs-up mr-1"></i>{{ blogPost.likes }}</a></small>
<small class="reply"><a><i class="fa fa-mail-reply mr-1"></i>Reply</a></small>
</div>
</section>
</article>
</div>
</div>
</div>
</div>
I hope this visualize my problem better.
for your first question it depends how you think about it, you want to disable the vote up? if yes then you need to treat it on the front end side, if not, you can the user let press the vote up and then in the server check for the user id and send a error message back ("you already voted"), for your second question i don't know very well what you mean with there is just 1 box? you mean you have 1 box component? if that makes you confusion, just think on the component as a class that you can instantiate in objects, same for components you can create a lot of since very one is a diferent instance.
I am making an application which will have form with dynamic input elements. With dynamic i mean that those inputs will come from the server side. Here's the psedo sample html :
<form>
<section1>
<static-field-1></static-field-1>
<static-field-2></static-field-2>
<dynamic-field-1></dynamic-field-1>
<dynamic-field-2></dynamic-field-2>
</section1>
<section2>//'a' postfix is just for differenting
<dynamic-field-1a></dynamic-field-1a>
<static-field-1a></static-field-1a>
<static-field-2a></static-field-2a>
<dynamic-field-2a></dynamic-field-2a>
</section2>
</form>
In short all the form rendering including their position will be decided by the api response from the server.
The api response will be something like this:
[
{
"section_pos":1,
"name":"section1",
"fields":[
{
"type":"static",
"name":"static-field-1"
},
{
"type":"static",
"name":"static-field-2"
},
{
"type":"dynamic",
"name":"dynamic-field-1",
"input":"text",
"validation":"required"
},
{
"type":"dynamic",
"name":"dynamic-field-2",
"input":"number",
"validation":"required"
}
]
},
{
"section_pos":2,
"name":"section2",
"fields":[
{
"type":"dynamic",
"name":"dynamic-field-1a",
"input":"text",
"validation":"required"
},
{
"type":"static",
"name":"static-field-1a"
},
{
"type":"static",
"name":"static-field-2a"
},
{
"type":"dynamic",
"name":"dynamic-field-2a",
"input":"number",
"validation":"required"
}
]
}
]
How will i create the dynamic input form elements and shuffle them in their respective section in accordance with the data sent from the server.
With the static i mean , these will be already present/known to us. Dynamic fields are the once that are sent by the server. Server also will sent the order in which each fields(static/dynamic) should be aligned.
Need help in this task
Hope I understood your question right.
If you want to list all fields as INPUT tag in server's response order:
<div *ngFor="let section of mySuperProvider.getDataFromServer()">
<h2>{{section.name}}</h2>
<p *ngFor="let field of section.fields">
<b>{{field.name}}</b>
<input type="text" ... >
</p>
</div>
If you want to shuffle fields, you can do it in provider or:
<p *ngFor="let field of shuffle(section.fields)">
Where shuffle() is a function:
shuffle(array) {
return array.sort(function() { return 0.5 - Math.random() });
}
If you want to show only specific types, and exclude others, then you can use *ngIf.
If you need instead of INPUT tag use Configurable Reactive Forms, then see nice article in this link, with a main idea to create dynamic component and pass config to it:
<form
class="dynamic-form"
[formGroup]="form"
(ngSubmit)="submitted.emit(form.value)">
<ng-container
*ngFor="let field of config;"
dynamicField
[config]="field"
[group]="form">
</ng-container>
</form>
I'm running into a problem with creating a dynamic class name based on the Angular 2 ngFor loop index. I had to use the following syntax because Angular 2 does not like ngFor and ngIf on the same element.
With this syntax, how can I create a dynamic class name with the value of index at {{index}}. I know this isn't proper A2 code, but I put it in my code example to show you where I would like the value to appear.
<div class="product-detail__variants">
<template ngFor #variant [ngForOf]="variants" #index="index">
<div *ngIf="currentVariant == index">
<div class="product-detail-carousel-{{index}}">
</div>
</div>
</template>
</div>
The value "variants" is an empty array of a set length. "variant" thus has no value.
"currentVariant" is a number that by default equals 0.
EDIT: This code above is correct. I had another extraneous error that I thought was connected to this code.
I don't really understand your problem ;-)
There are two ways to set classes for a specific element:
The way you do with curly brackets:
#Component({
selector: 'my-app',
template: `
<div class="product-detail__variants">
<template ngFor #variant [ngForOf]="variants" #index="index">
<div *ngIf="currentVariant == index">
<div class="product-detail-carousel-{{index}}">
Test
</div>
</div>
</template>
</div>
`,
styles: [
'.product-detail-carousel-2 { color: red }'
]
})
Test is displayed only for the third element (index 2) and in red.
As suggested by #Langley using the ngClass directive
import {NgClass} from 'angular2/common';
#Component({
selector: 'my-app',
template: `
<div class="product-detail__variants">
<template ngFor #variant [ngForOf]="variants" #index="index">
<div *ngIf="currentVariant == index">
<div class="product-detail-carousel-{{index}}">
Test
</div>
</div>
</template>
</div>
`,
styles: [
'.product-detail-carousel-2 { color: red }'
],
directives: [ NgClass ]
})
The different is that you need to specify the NgClass directive within the providers attribute of your component. Again Test is displayed only for the third element (index 2) and in red.
Your following sentences: "The value variants is an empty array of a set length. variant thus has no value. currentVariant is a number that by default equals 0.". How do you expect something to be displayed if your array is empty. An ngFor is an iteration...
Hope it helps you,
Thierry
In case anyone is looking to add a specific class based on the index, you can do something like this. I personally prefer the syntax here too. Let's say you wanted to add last-para to the last paragraph from an array:
<p
*ngFor="let p of paragraphs; let i = index"
[class.last-para]="i >= paragraphs.length - 1">
{{p}}</p>
I have the following problem, I want to call a function of another controller from within a controller I want to use for a guided tour (I'm using ngJoyRide for the tour). The function I want to call in the other controller is so to say a translator (LanguageController), which fetches a string from a database according to the key given as parameter. The LanguageController will, if the key is not found, return an error that the string could not be fetched from the database. In my index.html fetching the string works, but I want to use it in the overlay element of my guided tour, which does not work, but only shows the "not fetched yet"-error of the LanguageController.
My index.html looks like this:
<body>
<div class="container-fluid col-md-10 col-md-offset-1" ng-controller="LangCtrl as lc" >
<div ng-controller="UserCtrl as uc" mail='#email' firstname='#firstname'>
<div ng-controller="GuidedTourCtrl as gtc">
<div ng-joy-ride="startJoyRide" config="config" on-finish="onFinish()" on-skip="onFinish()">
...
{{lc.getTerm('system_lang_edit')}}
...
</div>
</div>
</div>
</div>
</body>
The controller I'm using for the guided Tour looks like this:
guidedTourModule.controller('GuidedTourCtrl',['$scope', function($scope) {
$scope.startJoyRide = false;
this.start = function () {
$scope.startJoyRide = true;
}
$scope.config = [
{
type: "title",
...
},
{
type: "element",
selector: "#groups",
heading: "heading",
text: " <div id='title-text' class='col-md-12'>\
<span class='main-text'>"\
+ $scope.lc.getTerm('system_navi_messages') + "\
text text text text\
</span>\
<br/>\
<br/>\
</div>",
placement: "right",
scroll: true,
attachToBody: true
}
];
...
}]);
And the output I ultimately get looks like this for the overlay element:
<div class="row">
<div id="pop-over-text" class="col-md-12">
<div id='title-text' class='col-md-12'>
<span class='main-text'>
not fetched yet: system_navi_messages
text text text text
</span>
<br/>
<br/>
</div>
</div>
</div>
...
I hope someone can see the error in my code. Thanks in advance!
Things needs clarity are,
How you defined the 'getTerm' function in your Language controller, either by using this.getTerm() or $scope.getTerm(). Since you are using alias name you will be having this.getTerm in Language controller.
Reason why you are able to access the getTerm function in your overlay element is, since this overlay element is inside the parent controller(Language Controller) and you are referencing it with alias name 'lc' while calling the getTerm function. Thats' why it is accessible.
But the string you pass as a parameter is not reachable to the parent controller. that's why the error message is rendered in the overlay HTML.
Please make a plunker of your app, so that will be helpful to answer your problem.