Angular 2 with *ngFor and ngIf to hide first element - javascript

In Angular 2, I got a list of items and based on a given condition (i.e based on location number) I am setting the list to repeat.
I need to hide the "remove text" for the first box each location.
Plunker:
https://plnkr.co/edit/xtX5BjTBjO61sD2tLwWN?p=preview:
[1]: https://plnkr.co/edit/xtX5BjTBjO61sD2tLwWN?p=preview
import {Component} from '#angular/core';
#Component({
selector: 'app',
template: `
<ul *ngFor="let locdata of locations">
<template ngFor let-item [ngForOf]="items"; let-i=index>
<div *ngIf="item.location==locdata.location">
<div class="title"> location {{ item.location}} <span *ngIf="isNotFirst(item)"> remove </span> </div>
<div class="container"> {{ item.sedule}}</div>
</div>
</template>
</ul>
`
})
export class App {
items: string[];
isNotFirst(item: any) {
return this.items.filter(i => i.location === item.location).map(i => i.id).indexOf(item.id) !== 0;
}
locations:any;
constructor() {
this.locations=[{location:1},{location:2},{location:3}];
this.items = [{
id:1,
location:1,
sedule:1
},
{
id:2,
location:2,
sedule:1
},
{
id:3,
location:1,
sedule:2
},
{
id:4,
location:2,
sedule:2
},
{
id:5,
location:1,
sedule:2
},
{
id:6,
location:2,
sedule:3
}, {
id:7,
location:3,
sedule:1
},
{
id:8,
location:3,
sedule:2
},
{
id:9,
location:3,
sedule:3
}];
}
}

assume you have a <span> in *ngFor logic, you can use first from *ngFor and show/hide <span> by *ngIf
<div *ngFor="let item in data; let first=first;">
{{item.attr}}
<span *ngIf="!first">
remove
</span>
</div>
your working plunker.

import {Component} from '#angular/core';
#Component({
selector: 'app',
template: `
<ul *ngFor="let locdata of locations">
<template ngFor let-item [ngForOf]="items"; let-i=index>
<div *ngIf="item.location==locdata.location">
<div class="title"> location {{ item.location}} <span *ngIf="isNotFirst(item)"> remove </span> </div>
<div class="container"> {{ item.sedule}}</div>
</div>
</template>
</ul>
`
})
export class App {
items: string[];
isNotFirst(item: any) {
return this.items.filter(i => i.location === item.location).map(i => i.id).indexOf(item.id) !== 0;
}
locations:any;
constructor() {
this.locations=[{location:1},{location:2},{location:3}];
this.items = [{
id:1,
location:1,
sedule:1
},
{
id:2,
location:2,
sedule:1
},
{
id:3,
location:1,
sedule:2
},
{
id:4,
location:2,
sedule:2
},
{
id:5,
location:1,
sedule:2
},
{
id:6,
location:2,
sedule:3
}, {
id:7,
location:3,
sedule:1
},
{
id:8,
location:3,
sedule:2
},
{
id:9,
location:3,
sedule:3
}];
}
}

After doing some deciphering of your template, I think that your basic html for a single item in your items collection looks like:
#Component({
selector: 'app',
template: `
<h4>Syntax 1</h4>
<div>
<div class="title">
location {{ item.location}}
<span *ngIf="true">remove</span>
</div>
<div class="container">
{{ item.sedule}}
</div>
</div>
`
})
export class App { .... }
If that's your basic template for a single item, you can then use *ngFor stamp out multiple versions of this HTML, one for each item in your items collection.
That gives you a template that looks like:
#Component({
selector: 'app',
template: `
<h4>Syntax 1</h4>
<div *ngFor="let item of items">
<div class="title">
location {{ item.location}}
<span *ngIf="true">remove</span>
</div>
<div class="container">
{{ item.sedule}}
</div>
</div>
`
})
export class App { .... }
The final piece to the puzzle is that you only want to show the remove text for a specific item in the collection - namely the first item. Thankfully, ngFor has a solution to this very same problem - you can ask ngFor to tell you the index of the current item in the list of items. You can make use of that index to show or hide the 'remove' text. Since the first item will have an index of 0, then your ngIf test will test against that value.
That gives you:
#Component({
selector: 'app',
template: `
<h4>Syntax 1</h4>
<ul>
<div *ngFor="let item of items; let i=index;">
<div class="title">
location {{ item.location}}
<span *ngIf="i != 0">remove</span>
</div>
<div class="container">
{{ item.sedule}}
</div>
</div>
</ul>
`
})
export class App { .... }
Notice the change in the ngFor statement - by using let i=index, you're creating a local template variable i that will be mapped to the current index of the item being output by ngFor. Then you can test against that value to show/hide an element if needed.
(Note that in addition to index, ngFor provides first, last, even and odd variables that you could also make use of. I used index in this example because it has the most wide reaching usage, but you could've easily used first for this use case.
See the documentation for more info about ngFor

See here:
https://plnkr.co/edit/nMvnonrBjRfq1n8tmfZT?p=preview
You need condition like:
*ngIf="i !== 1"

Related

How do I loop over a slice of an array using ngFor?

I am creating a FAQ page with accordion buttons, with groups of buttons under sub-headers. I designed it using an ngFor statement in the faq.html file.
<h1>Frequently Asked Questions</h1>
<div *ngFor="let item of data; let i = index;">
<button class="accordion" (click)="toggleAccordion($event, i)"> {{item.parentName}} </button>
<div class="panel" *ngFor="let child of item.childProperties" hide="!item.isActive">
<p> {{child.propertyName}} </p>
</div>
Here is a snippet of the faq.ts file.
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-faq',
templateUrl: './faq.html',
styleUrls: ['./faq.scss']
})
export class FaqComponent implements OnInit {
data: any = [
{
parentName: 'Question 1A',
childProperties: [{ propertyName: 'Answer 1A' }],
},
{
parentName: 'Question 2A',
childProperties: [{ propertyName: 'Answer 2A' }],
},
{
parentName: 'Question 3A',
childProperties: [{ propertyName: 'Answer 3A' }],
},
{
parentName: 'Question 1B',
childProperties: [{ propertyName: 'Answer 1B' }],
},
{
parentName: 'Question 2B',
childProperties: [{ propertyName: 'Answer 2B' }],
},
];
}
I want to put sub-headers over Section A (Questions 1A, 2A, and 3A), and Section B (Questions 1B and 2B). I think I can use slice in the ngFor statement in faq.html, but the code won't compile.
I tried this slice pipe:
<div *ngFor="let item of data | slice:0:2; let i = index;">
What should I change to get it to compile and break up the FAQ sections? Is the slice pipe the way to go?
Slicing Your Data
The problem here is that the slice pipe returns your data as type unknown. There are a couple of ways around this:
$any
<p *ngFor="let item of data | slice:2:4">
{{ $any(item).parentName }}
</p>
Bracket notation
<p *ngFor="let item of data | slice:2:4">
{{ item['parentName'] }}
</p>
A function
slicedData(data : any[]) : any[] {
return data.slice(2,4)
}
<p *ngFor="let item of slicedData(data)">
{{ item['parentName'] }}
</p>
You might want to properly type your data though, instead of using any. It is called Typescript for a reason after all.
Here are some examples in a Stackblitz.
I had to change the html to access the properties in different way and it got compiled:
<div *ngFor="let item of data; let i = index;">
<button class="accordion" (click)="toggleAccordion($event, i)"> {{item['parentName']}} </button>
<div class="panel" *ngFor="let child of item['childProperties'] | slice:0:2; let i = index;" hide="!item.isActive">
<p> {{child['propertyName']}} </p>
</div>
You can just add one *ngIf and check if i < 3:
<h1>Frequently Asked Questions</h1>
<div *ngFor="let item of data; let i = index;">
<ng-container *ngIf="i < 3">
<button class="accordion" (click)="toggleAccordion($event, i)"> {{item.parentName}} </button>
<div class="panel" *ngFor="let child of item.childProperties" hide="!item.isActive">
<p> {{child.propertyName}} </p>
</ng-container>
</div>
Thank you for your help, everyone. I changed faq.html to:
<h1>Frequently Asked Questions</h1>
<h2>General</h2>
<div *ngFor="let item of data; let i = index;">
<ng-container *ngIf="i < 3">
<button class="accordion" (click)="toggleAccordion($event, i)"> {{item.parentName}} </button>
<div class="panel" hide="!item.isActive">
<p> {{item.childName}} </p>
</div>
and it worked.

How can I work around the limitation of multiple root elements in Vue.js? [duplicate]

This question already has answers here:
A way to render multiple root elements on VueJS with v-for directive
(6 answers)
Closed 2 years ago.
hopefully someone here will be able to help me with this problem.
I have the following data:
[
{
title: 'Header',
children: [
{
title: 'Paragraph',
children: [],
},
],
},
{
title: 'Container',
children: [
{
title: 'Paragraph',
children: [],
},
],
},
]
I want to render this in a list of <div> like this:
<div class="sortable-item" data-depth="1" data-index="0">Header</div> <!-- Parent -->
<div class="sortable-item" data-depth="2" data-index="0">Paragraph</div> <!-- Child-->
<div class="sortable-item" data-depth="1" data-index="1">Container</div> <!-- Parent -->
<div class="sortable-item" data-depth="2" data-index="0">Paragraph</div> <!-- Child-->
I have built a component that would be recursive, this is what I have so far:
<template>
<template v-for="(item, index) in tree">
<div
class="sortable-item"
:data-depth="getDepth()"
:data-index="index"
:key="getKey(index)"
>
{{ item.title }}
</div>
<Multi-Level-Sortable
:tree="item.children"
:parent-depth="getDepth()"
:parent-index="index"
:key="getKey(index + 0.5)"
></Multi-Level-Sortable>
</template>
</template>
<script>
export default {
name: 'MultiLevelSortable',
props: {
tree: {
type: Array,
default() {
return [];
},
},
parentDepth: {
type: Number,
},
parentIndex: {
type: Number,
},
},
methods: {
getDepth() {
return typeof this.parentDepth !== 'undefined' ? this.parentDepth + 1 : 1;
},
getKey(index) {
return typeof this.parentIndex !== 'undefined' ? `${this.parentIndex}.${index}` : `${index}`;
},
},
};
</script>
As you can see not only I have a <template> as the root element I also have a v-for, two "no no" for Vue.js. How can I solve this to render the list of elements like I pointed out above?
Note: I have tried vue-fragment and I was able to achieve the structure I wanted, but then when I tried using Sortable.js it didn't work, as if it wouldn't recognise any of the .sortable-item elements.
Any help will be greatly appreciated! Thank you!
Thanks to #AlexMA I was able to solve my problem by using a functional component. Here is what it looks like:
import SortableItemContent from './SortableItemContent.vue';
export default {
functional: true,
props: {
tree: {
type: Array,
default() {
return [];
},
},
},
render(createElement, { props }) {
const flat = [];
function flatten(data, depth) {
const depthRef = typeof depth !== 'undefined' ? depth + 1 : 0;
data.forEach((item, index) => {
const itemCopy = item;
itemCopy.index = index;
itemCopy.depth = depthRef;
itemCopy.indentation = new Array(depthRef);
flat.push(itemCopy);
if (item.children.length) {
flatten(item.children, depthRef);
}
});
}
flatten(props.tree);
return flat.map((element) => createElement('div', {
attrs: {
'data-index': element.index,
'data-depth': element.depth,
class: 'sortable-item',
},
},
[
createElement(SortableItemContent, {
props: {
title: element.title,
indentation: element.indentation,
},
}),
]));
},
};
The SortableItemContent component looks like this:
<template>
<div class="item-content">
<div
v-for="(item, index) in indentation"
:key="index"
class="item-indentation"
></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">{{ title }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'SortableItemContent',
props: {
title: String,
indentation: Array,
},
};
</script>
Given the data I have posted on my question, it now renders the HTML elements like I wanted:
<div data-index="0" data-depth="0" class="sortable-item">
<div class="item-content">
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Header</div>
</div>
</div>
</div>
<div data-index="0" data-depth="1" class="sortable-item">
<div class="item-content">
<div class="item-indentation"></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Paragraph</div>
</div>
</div>
</div>
<div data-index="1" data-depth="0" class="sortable-item">
<div class="item-content">
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Container</div>
</div>
</div>
</div>
<div data-index="0" data-depth="1" class="sortable-item">
<div class="item-content">
<div class="item-indentation"></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Paragraph</div>
</div>
</div>
</div>
Thank you again #AlexMA for the tip on Functional Components.

How do I fetch JSON data with Vue and Axios

I'm trying to fetch product data from a JSON file, but can't get it to work.
I've tried several things and searched the internet for a solution but none of the examples on the internet equals my situation.
I'm new to both vue and axios, so please excuse my ignorance.
This is what I have so far:
Vue.component('products',{
data: {
results: []
},
mounted() {
axios.get("js/prods.json")
.then(response => {this.results = response.data.results})
},
template:`
<div id="products">
<div class="productsItemContainer" v-for="product in products">
<div class="productsItem">
<div class="">
<div class="mkcenter" style="position:relative">
<a class="item">
<img class="productImg" width="120px" height="120px" v-bind:src="'assets/products/' + product.image">
<div class="floating ui red label" v-if="product.new">NEW</div>
</a>
</div>
</div>
<div class="productItemName" >
<a>{{ product.name }}</a>
</div>
<div class="mkdivider mkcenter"></div>
<div class="productItemPrice" >
<a>€ {{ product.unit_price }}</a>
</div>
<div v-on:click="addToCart" class="mkcenter">
<div class="ui vertical animated basic button" tabindex="0">
<div class="hidden content">Koop</div>
<div class="visible content">
<i class="shop icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
`
})
new Vue({
el:"#app",
});
The json file is as follows
{
"products":[
{
"name": "Danser Skydancer",
"inventory": 5,
"unit_price": 45.99,
"image":"a.jpg",
"new":true
},
{
"name": "Avocado Zwem Ring",
"inventory": 10,
"unit_price": 123.75,
"image":"b.jpg",
"new":false
}
]
}
The problem is only with the fetching of the data from a JSON file, because the following worked:
Vue.component('products',{
data:function(){
return{
reactive:true,
products: [
{
name: "Danser Skydancer",
inventory: 5,
unit_price: 45.99,
image:"a.jpg",
new:true
},
{
name: "Avocado Zwem Ring",
inventory: 10,
unit_price: 123.75,
image:"b.jpg",
new:false
}
],
cart:0
}
},
template: etc.........
As the warnings suggest, please do the following:
Rename the data array from results to products since you are referencing it by the latter one as a name during render.
Make your data option a function returning an object since data option must be a function, so that each instance can maintain an independent copy of the returned data object. Have a look at the docs on this.
Vue.component('products', {
data() {
return {
products: []
}
},
mounted() {
axios
.get("js/prods.json")
.then(response => {
this.products = response.data.products;
});
},
template: `
//...
`
}
<div id="products">
<div class="productsItemContainer" v-for="product in products">
<div class="productsItem">
...
Also, since you're not using CDN (I think), I would suggest making the template a component with a separate Vue file rather than doing it inside template literals, something like that:
Products.vue
<template>
<div id="products">
<div class="productsItemContainer" v-for="product in products">
<div class="productsItem">
<!-- The rest of the elements -->
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Products',
data() {
return {
products: []
}
},
mounted() {
axios
.get("js/prods.json")
.then(response => {
this.products = response.data.products;
});
}
}
</script>
And then in your main JS file or anywhere else requiring this component:
import Products from './components/Products.vue';
new Vue({
el: '#app',
data() {
return {
//...
}
},
components: {
Products
}
})
<div id="app">
<Products />
</div>

How to change CSS class of a column based on the number of items in a JavaScript array?

I want to change a CSS class like col-md-12 based on the number of items in an array. I am pushing items to an array.
For example, when there is one item in the array, the class must be col-md-12, but when there are more items, the class must be col-md-6. There should be two columns with the class col-md-6.
<div class="row footer" *ngIf="model.component.length!=undefined">
<div class="col-md-{{getNoOfCols(model.component.length)}}" *ngFor="let item of model.component" style="margin-top:-25px;">
<all-component [model]="item" (Click1)="onComponentClick($event)" [selectedId]="targetBuilderId"></all-component>
</div>
</div>
You can ng-class
Example :
ng-class="{
'col-md-12' : (getNoOfCols(model.component.length) === 1),
'col-md-6' : (getNoOfCols(model.component.length) > 1)
}"
You can use ngIf to show divs according to your array length
<div class="row footer" *ngIf="model.component !=undefined">
<div *ngIf="model.component.length < 2; else colSix">
<div class="col-md-12">Code for col-md-12</div>
</div>
<ng-template #colSix>
<div class="col-md-6">
<div class="col-md-6">
</ng-template>
</div>
try to avoid calling a function inside your template and use ngClass to switch between the classes
import {Component, OnInit} from '#angular/core';
#Component({
selector: 'sample-app',
template: `
<div class="row footer">
<div *ngFor="let item of arr"
[ngClass]='{"col-md-12": arr.length == 1, "col-md-6": arr.length > 1}'
>
{{item}} hello
</div>
</div>
`
})
export class AppComponent implements OnInit{
arr: any[];
ngOnInit() {
this.arr = ['item1', 'item2', 'item3'];
}
}

Splicing array nested within ng-repeat,

The array is structured like so
$scope.structure = {
sections: [{
id:0,
sectionItems: []
},{
id:1,
sectionItems: []
},{
id:2,
sectionItems: []
}]
};
I have a nested ng-repeat so I can show items within sectionItems[]
(Inside should be objects and one of the keys is Name - not relevant)
<div ng-repeat="sections in structure.sections" class="col-md-12">
<div class="panel panel-info">
<ul class="screenW-section">
<li class="col-xs-6" ng-repeat="item in sections.sectionItems"
ng-click="item.splice($index, 1)">
{{item.Name}}
</li>
</ul>
</div> </div>
I want to be able to remove items on click but the
ng-click="item.splice($index, 1)
Is not working no matter how I format it.
try this:
var app = angular.module("testApp", []);
app.controller('testCtrl', function($scope){
$scope.structure = {
sections: [{
id:0,
sectionItems: ['1','2','3']
},{
id:1,
sectionItems: ['11','21','32']
},{
id:2,
sectionItems: ['12','22','32']
}]
};
$scope.remove = function(sectionItems,index){
sectionItems.splice(index, 1);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="testApp" ng-controller="testCtrl">
<div ng-repeat="sections in structure.sections" class="col-md-12">
<div class="panel panel-info">
<ul class="screenW-section">
<li class="col-xs-6" ng-repeat="item in sections.sectionItems"
ng-click="remove(sections.sectionItems,$index)">
{{item}}
</li>
</ul>
</div> </div>
</div>
To remove an item you need to remove it from the array.
So, for example, you could do
ng-click="remove(sections.sectionItems, $index)"
in the view, and
$scope.remove = function(array, index) {
array.splice(index, 1);
}
in the controller...
You're calling splice on the item and not the array
ng-click="sections.sectionItems.splice($index, 1)"

Categories