Angular 2. Issue with *ngFor, when i use Pipe - javascript

I have a component.
#Component({
selector: 'top3',
templateUrl: 'dev/templates/top3.html',
pipes: [orderBy],
providers: [HTTP_PROVIDERS, ParticipantService]
})
export class AppTop3Component implements OnInit {
constructor (private _participantService: ParticipantService) {}
errorMessage: string;
participants: any[];
ngOnInit() {
this.getParticipants();
}
getParticipants() {
this._participantService.getParticipants()
.then(
participants => this.participants = participants,
error => this.errorMessage = <any>error
);
}
}
This component use a service, named _participantService. The _participantService retrieves an array of objects. I output my array of objects in component`s template:
<h2>Top 3</h2>
<table class="table table-bordered table-condensed" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr *ngFor="#participant of participants | orderBy: '-score'; #i = index">
<td>{{i+1}}</td>
<td>{{participant.username}}</td>
<td>{{participant.score}}</td>
</tr>
</tbody>
</table>
I use a Pipe, named orderBy with *ngFor directive. The problem is when I don`t use a Pipe and output array in this way:
<tr *ngFor="#participant of participants; #i = index">
everything is OK, and I've got a correct result:
But when I want to sort my object's array and use my Pipe, I don't have any output:
I`ve got an undefined object in my Pipe function^
#Pipe({
name: 'orderBy',
pure: false
})
export class orderBy implements PipeTransform {
transform(arr: any[], orderFields: string[]): any {
console.log(arr);
orderFields.forEach(function(currentField: string) {
var orderType = 'ASC';
if (currentField[0] === '-') {
currentField = currentField.substring(1);
orderType = 'DESC';
}
arr.sort(function(a, b) {
return (orderType === 'ASC') ?
(a[currentField] < b[currentField]) ? -1 :
(a[currentField] === b[currentField]) ? 0 : 1 :
(a[currentField] < b[currentField]) ? 1 :
(a[currentField] === b[currentField]) ? 0 : -1;
});
});
return arr;
}
}

Since you load your data asynchronously using a promise, they are null at the beginning (before the first callback defined in the then method is called).
You need to check if the are parameter is null in your pipe. If so you shouldn't execute the "order by" processing...
When the result will be there, the pipe will be called again with the data (are won't be null). In this case, you will be able to sort data...
You could try this code:
#Pipe({
name: 'orderBy',
pure: false
})
export class orderBy implements PipeTransform {
transform(arr: any[], orderFields: string[]): any {
if (arr==null) {
return null;
}
(...)
}
}

According to an example of the doc of the Pipes (https://angular.io/docs/ts/latest/guide/pipes.html#!#jsonpipe), you missed parenthesis :
<tr *ngFor="#participant of (participants | orderBy: '-score'); #i = index">

Instead of modifying your pipe implementation to handle null (i.e., #Thierry's answer), you could instead simply define an empty array in your component:
participants = [];
When the data comes back from the server, either push items onto the existing array,
.then(participants => this.participants.push(...participants),
or do what you already do: assign a new array.
Note that ... is an ES2015 feature.

Related

The Input() data passed in child component remains same after changing in the child

So i am passing an object having length of data and the data itself which is an array of objects
Now what is happening is that when I change the data in the child component and reload the page the data is still showing the edited property
To clarify more see the following screenshots:
When we navigate to the route for the first time there would be no modified data:
When I edit the data::
As you can see on the right side finalAmount property is added and its value is initialised
Now when renavigate to the same component finalAmount is set to 5
The property itself should not be present in the data as I am getting it from the parent
These are the relevant files:
stepper.component.html (Parent Component)
<mat-step [stepControl]="thirdFormGroup" >
<ng-template matStepLabel>Prepare Invoice</ng-template>
<ng-template matStepContent>
<div class="container-fluid">
<app-create-invoice [selectedCustomersInfo]="selectedCustomerData"
[selectedProductsData]="{lengthOfData:cartItems.length , selectedProducts:cartItems}"></app-create-invoice>
</div>
</ng-template>
</mat-step>
stepper.component.ts:
#Component({
selector: 'app-stepper',
templateUrl: './stepper.component.html',
styleUrls: ['./stepper.component.scss'],
providers: [
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: { showError: true }
}
]
})
export class StepperComponent implements OnInit, OnDestroy {
getProductsSubscription = new Subscription
cartItems: ProductDataModel[] = []
selectedCustomerData: any
products = this._formBuilder.group({
firstCtrl: ['', Validators.required],
});
thirdFormGroup = this._formBuilder.group({
thirdCtrl: ['', Validators.required],
});
stepperOrientation: Observable<StepperOrientation>;
constructor(private _formBuilder: FormBuilder, breakpointObserver: BreakpointObserver, private cartService: CartService) {
this.stepperOrientation = breakpointObserver
.observe('(min-width: 800px)')
.pipe(map(({ matches }) => (matches ? 'horizontal' : 'vertical')));
}
ngOnDestroy(): void {
console.log("Stepper destroyed");
this.getProductsSubscription.unsubscribe()
}
ngOnInit(): void {
this.getProductsSubscription = this.cartService.getProducts().subscribe((items) => {
this.cartItems = [...items]
console.log("Cart items::", this.cartItems)
})
}
setCustomerData(customerData: any) {
this.selectedCustomerData = customerData
}
}
create-invoice.component.html (Child Component):
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Product Category</th>
<th scope="col">Sub Category</th>
<th scope="col">Master Category</th>
<th scope="col">Product Weight</th>
<th scope="col">Price</th>
<th scope="col">Labour</th>
<th scope="col">SGST (In %)</th>
<th scope="col">CGST (In %)</th>
<th scope="col">Discount</th>
<th scope="col">Final Amount</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of _selectedProductsData;index as i">
<th scope="row">{{i+1}}</th>
<td>{{item.productCategory}}</td>
<td>{{item.subCategory}}</td>
<td>{{item.masterCategory}}</td>
<td>{{item.productWeight}} gms</td>
<td>
<input class="form-control priceInput" type="number" id="{{item.productGuid}}-price"
min="0" (input)="item.price = getValue($event,item.id,'price')" #price
placeholder="Enter Price">
</td>
<td>
<input class="form-control labourInput" type="number" id="{{item.productGuid}}-labour"
min="0" (input)="item.labour = getValue($event,item.id,'labour')" #labor
placeholder="Enter Labour">
</td>
<td>
<input class="form-control sgstInput" type="number" id="{{item.productGuid}}-SGST"
min="0" (input)="item.SGST = getValue($event,item.id,'sgst')" #sgst
placeholder="Enter SGST">
</td>
<td>
<input class="form-control cgstInput" type="number" id="{{item.productGuid}}-CGST"
min="0" (input)="item.CGST = getValue($event,item.id,'cgst')" #cgst
placeholder="Enter CGST">
</td>
<td>
<input class="form-control discountInput" type="number" min="0"
(input)="item.discount = getValue($event,item.id,'discount')" #discount
id="{{item.productGuid}}-discount" placeholder="Enter Discount">
</td>
<td>
{{ item.finalAmount ?? 0 }}
<input class="form-control" type="hidden" [value]="item.finalAmount ?? 0">
</td>
</tr>
</tbody>
</table>
</div>
create-invoice.component.ts:
#Component({
selector: 'app-create-invoice',
templateUrl: './create-invoice.component.html',
styleUrls: ['./create-invoice.component.scss']
})
export class CreateInvoiceComponent implements OnInit,OnDestroy {
_selectedCustomersInfo:any
_selectedProductsData:InvoiceProductDataModel[] = []
totalWeight = 0
totalDiscount = 0
totalGST = 0
totalAmountWithGST = 0
currentDate:Date = new Date()
#Input() set selectedProductsData(productsData: {lengthOfData:number,selectedProducts:ProductDataModel[]}) {
this.totalWeight = 0
this.totalDiscount = 0
this.totalAmountWithGST = 0
this.totalGST = 0
var temp = Object.assign({},productsData)
this._selectedProductsData = []
this._selectedProductsData = [...temp.selectedProducts]
// this._selectedProductsData = [...productsData.selectedProducts]
this._selectedProductsData.forEach((product) => {
this.totalWeight += product.productWeight
})
console.log(this._selectedProductsData)
}
#Input() set selectedCustomersInfo(customerInfo: any) {
this._selectedCustomersInfo = customerInfo
}
constructor() { }
ngOnDestroy(): void {
console.log("Create Invoice Destroyed!!")
this._selectedProductsData = []
}
ngOnInit(): void {
}
getValue(event: Event, productId:number, valueOf:string): number {
let product = this._selectedProductsData.find(item => item.id === productId)
if (product) {
switch (valueOf) {
case 'labour':
product.labour = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
break
case 'price':
product.price = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
break
case 'sgst':
product.SGST = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
this.setTotalGST()
break
case 'cgst':
product.CGST = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
this.setTotalGST()
break
case 'discount':
product.discount = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalDiscount()
this.setTotalAmountWithGST()
break
}
}
console.log(this._selectedProductsData.find(i => i.id == productId))
return Number((event.target as HTMLInputElement).value);
}
setFinalAmountOfEachProduct(product:InvoiceProductDataModel) {
product.finalAmount = 0
let partialSum = (product.labour ?? 0) + (product.price ?? 0) - (product.discount ?? 0)
let cgst = product.CGST ? partialSum * ((product.CGST ?? 100) / 100) : 0
let sgst = product.SGST ? partialSum * ((product.SGST ?? 100) / 100) : 0
product.totalGST = cgst + sgst
product.finalAmount = partialSum + cgst + sgst
}
setTotalDiscount() {
this.totalDiscount = 0
this._selectedProductsData.forEach((item)=> {
this.totalDiscount += item.discount ?? 0
})
}
setTotalAmountWithGST() {
this.totalAmountWithGST = 0
this._selectedProductsData.forEach((item)=> {
this.totalAmountWithGST += item.finalAmount ?? 0
})
}
setTotalGST() {
this.totalGST = 0
this._selectedProductsData.forEach((item)=> {
this.totalGST += item.totalGST ?? 0
})
}
}
And its not limited to finalAmount property it happens with other properties too like discount,sgst etc..
Any help will be appreciated
From what I've understood you've got a problem with some data that have been changed in parent but it's not updated in child component, that have received it thru #input(). If I am right, you should read about Angular lifecycle hooks: HERE. Basically what you need to do, is to implement change detection hooks
#Input() someInputData: any;
ngOnChanges(changes: SimpleChanges) {
this.doSomething(changes.someInputData.currentValue);
}
The alternative is to use getter-setter approach like that
private _someInputData: string;
#Input() set someInputData(value: any) {
this._someInputData = value;
this.doSomething(this._someInputData);
}
get someInputData(): any {
return this._someInputData;
}
Which approach is better? The short answer is I don't know :) I haven't measure performance difference, but:
ngOnChanges() will allow you to compare current and prev value
ngOnChanges() will track all inputs in comparison to getter-setter approach
However, there are some particular scenarios, usually with nested objects that tend to resist change detection in Angular, and for more elaborate solution see: SOLUTION
EDIT!
Based on your comments, now I understand your problem, however I do not feel competent enough to tell you, why exactly such thing is occurring, it's related to the fact how Objects are made. Basically while passing Object through #Input decorator, you're creating a copy of object itself with all of its references. TLDR: Object passed by #Input() still holds a reference to memory that holds some variable. In that case what you need to do, is to create a deep copy of object and all of Objects within this object, because otherwise the nested references are still there. I see, that you're trying to mitigate this behaviour by using Object.assign(), which to my knowledge should work. However instead of doing so, please try using another approach:
const tempClone = JSON.parse(JSON.stringify(productsData));
We can use structuredClone() to deep copy nested objects here in this case as my object having an array to a key
Refer the following screenshot as well:
Also JSON.parse(JSON.stringify(productsData)) has its own limitations
For more information refer these links:
https://stackoverflow.com/a/5344074/18480147
https://stackoverflow.com/a/10916838/18480147

how to read the array of data in html templates

I have three variables and i was created one array and pushed all these three variables in to this array.
And in my html template i am using a table. I tried with *ngFor but it is not working and also i tried with string interpolation and that too not woriking.
I am getting an error called <--can not read property "title" of undefined-->
In my console i am getting data like this....
array of data
(3)[{...}{...}{...}]
0:{key:"-LtZprPfMtDgzuxmaI5o"}
1:{price:1}
2:{title:"title1"}
array of data
(3)[{...}{...}{...}]
0:{key:"-LtcY8_6dd8fzibK9EYV"}
1:{price:2}
2:{title:"title2"}
array of data
(3)[{...}{...}{...}]
0:{key:"-LtcZF9NHknDZ0OcKzHg"}
1:{price:3}
2:{title:"title3"}
And here my product.component.ts
import { ProductsService } from './../../products.service';
import { Component, OnInit } from '#angular/core';
import { AngularFireDatabase, AngularFireAction, DatabaseSnapshot } from 'angularfire2/database';
#Component({
selector: 'app-admin-products',
templateUrl: './admin-products.component.html',
styleUrls: ['./admin-products.component.css']
})
export class AdminProductsComponent{
constructor(
private products:ProductsService,
private db : AngularFireDatabase
) {
products.getAll().on('child_added',function(c){
let data = c.val();
let key = c.key;
let price = data.price;
let title = data.title;
let array=[];
array.push({"key":key});
array.push({"title":title});
array.push({"price":price});
console.log('array of data',array);
})
}
}
And my admin-products.component.html
<table class="table">
<thead>
<th>Title</th>
<th>Price</th>
<th></th>
</thead>
<tbody *ngFor="let p of array">
<td>{{p.title}}</td>
<td>{{p.price}}</td>
<td ><a [routerLink]="['/admin/products',p.key]">Edit</a></td>
</tbody>
</table>
From your html code, I think you need a array of object.
for example :
array = [
{key: 'key1', title: 'title 1', price: 10, },
{key: 'key2', title: 'title 2', price: 10, },
{key: 'key3', title: 'title 3', price: 10, }
]
if my assumption is correct then there are several problems here.
array construction is not ok.
your array variable is local. make it a public variable otherwise from html you can't access it.
In html, you are trying to iterate the array and each iteration, the data will be assigned in your local variable 'p' (as you are using *ngFor="let p of array")
so, change your code to this
import { ProductsService } from './../../products.service';
import { Component, OnInit } from '#angular/core';
import { AngularFireDatabase, AngularFireAction, DatabaseSnapshot } from 'angularfire2/database';
#Component({
selector: 'app-admin-products',
templateUrl: './admin-products.component.html',
styleUrls: ['./admin-products.component.css']
})
export class AdminProductsComponent{
array: any = [];
constructor(
private products:ProductsService,
private db : AngularFireDatabase
) {
products.getAll().on('child_added',function(c){
let data = c.val();
let key = c.key;
let price = data.price;
let title = data.title;
this.array.push({"key":key, "title":title, "price":price });
console.log('array of data', this.array);
})
}
}
and change your html to this,
<table class="table">
<thead>
<th>Title</th>
<th>Price</th>
<th></th>
</thead>
<tbody *ngFor="let p of array">
<td>{{p.title}}</td>
<td>{{p.price}}</td>
<td ><a [routerLink]="['/admin/products',p.key]">Edit</a></td>
</tbody>
</table>
Change this:
<tbody *ngFor="let p of array">
<td>{{array.title}}</td>
<td>{{array.price}}</td>
into this:
<tbody *ngFor="let p of array">
<td>{{p.title}}</td>
<td>{{p.price}}</td>
You have an array of object, therefore you need to use ngFor to iterate inside the array and then use the variable p to access the attribute's values.
Try like this:
Have the loop in tr instead of td
Change {{array.title}} to {{p.title}}
Make array global
.html
<tbody>
<tr *ngFor="let p of array">
<td>{{p.title}}</td>
<td>{{p.price}}</td>
<td ><a [routerLink]="['/admin/products',p.key]">Edit</a></td>
</tr>
</tbody>
.ts
export class AdminProductsComponent{
array:any[] = [];
products.getAll().on('child_added',function(c){
let data = c.val();
let key = c.key;
let price = data.price;
let title = data.title;
this.array=[];
this.array.push({"key":key});
this.array.push({"title":title});
this.array.push({"price":price});
console.log('array of data',this.array);
})
}
array is a local variable inside your callback. Make it a member of your component to make it available in the template.
export class AdminProductsComponent{
array: any[] = [];
constructor(
private products:ProductsService,
private db : AngularFireDatabase
) {
products.getAll().on('child_added', c => {
let data = c.val();
let key = c.key;
let price = data.price;
let title = data.title;
// you probably want one item per product, not three??
this.array.push({ key, title, price });
})
}
}
Template:
<tbody >
<tr *ngFor="let p of array">
<td>{{p.title}}</td>
</tr>
</tbody>
Define an array property in your component and pass objects into it.
Use Arrow function for the on callback so your this context is the component.
import { ProductsService } from './../../products.service';
import { Component, OnInit } from '#angular/core';
import { AngularFireDatabase, AngularFireAction, DatabaseSnapshot } from 'angularfire2/database';
#Component({
selector: 'app-admin-products',
templateUrl: './admin-products.component.html',
styleUrls: ['./admin-products.component.css']
})
export class AdminProductsComponent{
// Define property here
public array = [],
constructor(
private products:ProductsService,
private db : AngularFireDatabase
) {
products.getAll().on('child_added',(c) => {
let data = c.val();
let key = c.key;
let price = data.price;
let title = data.title;
this.array.push({key, title, price});
})
}
}

how to create a custom sorting pipe in angular 4

how to create a custom sorting pipe in angular 4
component.ts
#Component({
selector: 'app-cinemas',
templateUrl: './cinemas.component.html',
encapsulation: ViewEncapsulation.None,
styleUrls: ['./cinemas.component.css']
})
export class CinemasComponent implements OnInit {
filteredCinema = [];
master() {
this.cinemaService.getCinMaster()
.subscribe(
(response: any) => {
this.filteredCinema = response;
},
// (error) => console.log(error)
);}
component.html
<div class="table-scrollable">
<table class="table table-striped table-bordered table-advance table-hover">
<thead>
<tr>
<th>Cinema</th>
<th>Cinema Operator</th>
<th>Capacity</th>
<th>Cinema Status</th>
<th>Unavailable From Date</th>
<th>Unavailable To Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Loop starts here -->
<tr class="cinema-row cathay" *ngFor="let cintable of filteredCinema | sorting:'cintable.cinema_name' ">
<td>
<p class="uppercase">{{cintable.cinema_name }}</p>
</td>
<td>
<img [src]="getOperatorImg(cintable.cinema_operator_id)" height="30">
</td>
<td>
<small>
Halls:
<strong>{{cintable.halls.length}}</strong>
</small>
</td>
<td>
<p class="uppercase">{{getStatusName(cintable.status)}}</p>
</td>
<td>
<p class="uppercase">{{cintable.available_date_from | date}}</p>
</td>
<td>
<p class="uppercase">{{cintable.available_date_to | date}} </p>
</td>
<td>
<p class="uppercase">{{cintable.entity}} </p>
</td>
<!-- Loop ends here -->
</tbody>
</table>
</div>
Pipe.ts
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'sorting'
})
export class SortingPipe implements PipeTransform {
transform(value: any, args?: any): any {
return null;
}
}
need to sort the cintable.cinema_name but not able to find the solution.
Here are the link i have gone through
http://hashtagcoder.com/an-introduction-to-angular-cli-part-3-sort-and-filter-data-with-custom-pipes/
https://hassantariqblog.wordpress.com/2017/03/16/angular2-creating-custom-sort-filter-pipe-for-datetime-column-ngfor-directive/
Please help me with this
The orderBy pipe was removed from Angular 2 because generally filtering logic like this should be done within the component. Since you already return filteredCinema from your component, it would be best to apply custom sorting here.
Assuming you want to have a case-insensitive sorting by cinema_name, do something like this in your subscribe callback:
this.filteredCinema = return items.sort((a, b) => {
let aLC: string = a.cinema_name.toLowerCase();
let bLC: string = b.cinema_name.toLowerCase();
return aLC < bLC ? -1 : (aLC > bLC ? 1 : 0);
});
(Also you should first ensure that response is not empty in this case, for simplicity I left that out)
If you wish filtering to be case-sensitive, simply remove the .toLowerCase calls.
If you still want to do it via a pipe, you should continue reading the tutorial first, because you are lacking the sorting mechanism.
Let's say you wanted a pipe orderByCinemaName:
#Pipe({
name: 'orderByCinemaName'
})
export class OrderByCinemaNamePipe implements PipeTransform {
transform(items: any[]): any[] {
return items.sort((a, b) => {
let aLC: string = a.cinema_name.toLowerCase();
let bLC: string = b.cinema_name.toLowerCase();
return aLC < bLC ? -1 : (aLC > bLC ? 1 : 0);
});
}
}
UPDATE
Fixed the sorting algorithm.

Referencing objects in HTML in Angular2

I am fetching data from an api server and storing it as a object in TypeScript, I need help referencing that data in HTML as a key and value pair.
TypeScript:
if (x.AttributeTemplateId==templateId) {
x['AttributeTemplateValue'].forEach(function(x){
console.log('hello');
if (x.AttributeTemplateId==templateId){
console.log(templateId);
(function(y){
console.log("hello2");
newName.push(y.CodeSet);
newValue.push(y.CodeValue);
// console.log([newAttributes]);
})(x);
}
})
}
})
newName = this.name;
newValue = this.value;
this.attributes = this.name.reduce((acc, value, i) => (acc[value] = this.value[i], acc), {});
console.log(this.attributes);
}
My data is in this.attributes and it looks like this
I want to put key and value in a table like
<table>
<tr>
<th> Name </th>
<th> Value </th>
</tr>
<tr key>
<td value > </td>
</tr>-->
</table>
How would I achieve this in Angular2 ?
step 1: Create a custom pipe that stores all the object keys and values in an array and returns them just like this
import { PipeTransform, Pipe} from '#angular/core';
#Pipe({name: objKeys})
export calss objKeysPipe implements PipeTransform {
transform(value, arguments: string[]) :any {
let keys= [];
for (let k in value){
keys.push({key: k, value: value[k]});
}
return keys;
}
setp 2: in your component template you do the folowing
<table>
<tr *ngFor="let att of attributes | objKeys">
<th>{{att.key}}</th>
<th>{{att.value}}</th>
</tr>
</table>

How to do *ng-for on an object with keys in Angular 2.0? [duplicate]

This question already has answers here:
access key and value of object using *ngFor
(21 answers)
Closed 1 year ago.
I am trying to do some things in Angular 2 Alpha 28, and am having an issue with dictionaries and ngFor.
I have an interface in TypeScript looking like this:
interface Dictionary {
[index: string]: string
}
In JavaScript this will translate to an object that with data might look like this:
myDict={'key1':'value1','key2':'value2'}
I want to iterate over this and tried this:
<div *ngFor="(#key, #value) of myDict">{{key}}:{{value}}</div>
But to no avail, none of the below worked either:
<div *ngFor="#value of myDict">{{value}}</div>
<div *ngFor="#value of myDict #key=index">{{key}}:{{value}}</div>
In all cases I get errors like Unexpected token or Cannot find 'iterableDiff' pipe supporting object
What am I missing here? Is this not possible anymore? (The first syntax works in Angular 1.x) or is the syntax different for iterating over an object?
Angular 6.1.0+ Answer
Use the built-in keyvalue-pipe like this:
<div *ngFor="let item of myObject | keyvalue">
Key: <b>{{item.key}}</b> and Value: <b>{{item.value}}</b>
</div>
or like this:
<div *ngFor="let item of myObject | keyvalue:mySortingFunction">
Key: <b>{{item.key}}</b> and Value: <b>{{item.value}}</b>
</div>
where mySortingFunction is in your .ts file, for example:
mySortingFunction = (a, b) => {
return a.key > b.key ? -1 : 1;
}
Stackblitz: https://stackblitz.com/edit/angular-iterate-key-value
You won't need to register this in any module, since Angular pipes work out of the box in any template.
It also works for Javascript-Maps.
It appears they do not want to support the syntax from ng1.
According to Miško Hevery (reference):
Maps have no orders in keys and hence they iteration is unpredictable.
This was supported in ng1, but we think it was a mistake and will not
be supported in NG2
The plan is to have a mapToIterable pipe
<div *ngFor"var item of map | mapToIterable">
So in order to iterate over your object you will need to use a "pipe".
Currently there is no pipe implemented that does that.
As a workaround, here is a small example that iterates over the keys:
Component:
import {Component} from 'angular2/core';
#Component({
selector: 'component',
templateUrl: `
<ul>
<li *ngFor="#key of keys();">{{key}}:{{myDict[key]}}</li>
</ul>
`
})
export class Home {
myDict : Dictionary;
constructor() {
this.myDict = {'key1':'value1','key2':'value2'};
}
keys() : Array<string> {
return Object.keys(this.myDict);
}
}
interface Dictionary {
[ index: string ]: string
}
try to use this pipe
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({ name: 'values', pure: false })
export class ValuesPipe implements PipeTransform {
transform(value: any, args: any[] = null): any {
return Object.keys(value).map(key => value[key]);
}
}
<div *ngFor="#value of object | values"> </div>
Updated : Angular is now providing the pipe for looping through a json Object via keyvalue :
<div *ngFor="let item of myDict | keyvalue">
{{item.key}}:{{item.value}}
</div>
WORKING DEMO , and for more detail Read
Previously (For Older Version) : Till now the best / shortest answer I found is ( Without any Pipe Filter or Custom function from Component Side )
Component side :
objectKeys = Object.keys;
Template side :
<div *ngFor='let key of objectKeys(jsonObj)'>
Key: {{key}}
<div *ngFor='let obj of jsonObj[key]'>
{{ obj.title }}
{{ obj.desc }}
</div>
</div>
WORKING DEMO
In addition to #obscur's answer, here is an example of how you can access both the key and value from the #View.
Pipe:
#Pipe({
name: 'keyValueFilter'
})
export class keyValueFilterPipe {
transform(value: any, args: any[] = null): any {
return Object.keys(value).map(function(key) {
let pair = {};
let k = 'key';
let v = 'value'
pair[k] = key;
pair[v] = value[key];
return pair;
});
}
}
View:
<li *ngFor="let u of myObject |
keyValueFilter">First Name: {{u.key}} <br> Last Name: {{u.value}}</li>
So if the object were to look like:
myObject = {
Daario: Naharis,
Victarion: Greyjoy,
Quentyn: Ball
}
The generated outcome would be:
First name: Daario
Last Name: Naharis
First name: Victarion
Last Name: Greyjoy
First name: Quentyn
Last Name: Ball
Adding to SimonHawesome's excellent answer. I've made an succinct version which utilizes some of the new typescript features. I realize that SimonHawesome's version is intentionally verbose as to explain the underlying details. I've also added an early-out check so that the pipe works for falsy values. E.g., if the map is null.
Note that using a iterator transform (as done here) can be more efficient since we do not need to allocate memory for a temporary array (as done in some of the other answers).
import {Pipe, PipeTransform} from '#angular/core';
#Pipe({
name: 'mapToIterable'
})
export class MapToIterable implements PipeTransform {
transform(map: { [key: string]: any }, ...parameters: any[]) {
if (!map)
return undefined;
return Object.keys(map)
.map((key) => ({ 'key': key, 'value': map[key] }));
}
}
Here's a variation on some of the above answers that supports multiple transforms (keyval, key, value):
import { Pipe, PipeTransform } from '#angular/core';
type Args = 'keyval'|'key'|'value';
#Pipe({
name: 'mapToIterable',
pure: false
})
export class MapToIterablePipe implements PipeTransform {
transform(obj: {}, arg: Args = 'keyval') {
return arg === 'keyval' ?
Object.keys(obj).map(key => ({key: key, value: obj[key]})) :
arg === 'key' ?
Object.keys(obj) :
arg === 'value' ?
Object.keys(obj).map(key => obj[key]) :
null;
}
}
Usage
map = {
'a': 'aee',
'b': 'bee',
'c': 'see'
}
<div *ngFor="let o of map | mapToIterable">{{o.key}}: {{o.value}}</div>
<div>a: aee</div>
<div>b: bee</div>
<div>c: see</div>
<div *ngFor="let o of map | mapToIterable:'keyval'">{{o.key}}: {{o.value}}</div>
<div>a: aee</div>
<div>b: bee</div>
<div>c: see</div>
<div *ngFor="let k of map | mapToIterable:'key'">{{k}}</div>
<div>a</div>
<div>b</div>
<div>c</div>
<div *ngFor="let v of map | mapToIterable:'value'">{{v}}</div>
<div>aee</div>
<div>bee</div>
<div>see</div>
I had a similar issue, built something for objects and Maps.
import { Pipe } from 'angular2/core.js';
/**
* Map to Iteratble Pipe
*
* It accepts Objects and [Maps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
*
* Example:
*
* <div *ngFor="#keyValuePair of someObject | mapToIterable">
* key {{keyValuePair.key}} and value {{keyValuePair.value}}
* </div>
*
*/
#Pipe({ name: 'mapToIterable' })
export class MapToIterable {
transform(value) {
let result = [];
if(value.entries) {
for (var [key, value] of value.entries()) {
result.push({ key, value });
}
} else {
for(let key in value) {
result.push({ key, value: value[key] });
}
}
return result;
}
}
Angular 2.x && Angular 4.x do not support this out of the box
You can use this two pipes to iterate either by key or by value.
Keys pipe:
import {Pipe, PipeTransform} from '#angular/core'
#Pipe({
name: 'keys',
pure: false
})
export class KeysPipe implements PipeTransform {
transform(value: any, args: any[] = null): any {
return Object.keys(value)
}
}
Values pipe:
import {Pipe, PipeTransform} from '#angular/core'
#Pipe({
name: 'values',
pure: false
})
export class ValuesPipe implements PipeTransform {
transform(value: any, args: any[] = null): any {
return Object.keys(value).map(key => value[key])
}
}
How to use:
let data = {key1: 'value1', key2: 'value2'}
<div *ngFor="let key of data | keys"></div>
<div *ngFor="let value of data | values"></div>
//Get solution for ng-repeat
//Add variable and assign with Object.key
export class TestComponent implements OnInit{
objectKeys = Object.keys;
obj: object = {
"test": "value"
"test1": "value1"
}
}
//HTML
<div *ngFor="let key of objectKeys(obj)">
<div>
<div class="content">{{key}}</div>
<div class="content">{{obj[key]}}</div>
</div>
If someone is wondering how to work with multidimensional object, here is the solution.
lets assume we have following object in service
getChallenges() {
var objects = {};
objects['0'] = {
title: 'Angular2',
description : "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
};
objects['1'] = {
title: 'AngularJS',
description : "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
};
objects['2'] = {
title: 'Bootstrap',
description : "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
};
return objects;
}
in component add following function
challenges;
constructor(testService : TestService){
this.challenges = testService.getChallenges();
}
keys() : Array<string> {
return Object.keys(this.challenges);
}
finally in view do following
<div *ngFor="#key of keys();">
<h4 class="heading">{{challenges[key].title}}</h4>
<p class="description">{{challenges[key].description}}</p>
</div>
I have been tearing my hair out with trying to parse and use data returned form a JSON query/ api call. Im not sure exactly where i was going wrong, i feel like i have been circling the answer for days, chasing various error codes like:
"Cannot find 'iterableDiff' pipe supporting object"
"Generic TYpe Array requires one argument(s)"
JSON parsing Errors, and im sure others
Im assuming i just had the wrong combination of fixes.
So here's a bit of a summary of gotchas and things to look for.
Firstly check the result of your api calls, your results may be in the form of an object, an array, or an array of objects.
i wont go into it too much, suffice to say the OP's original Error of not being iterable is generally caused by you trying to iterate an object, not an Array.
Heres some of my debugging results showing variables of both arrays and objects
So as we generally would like to iterate over our JSON result we need to ensure it is in the form of an Array. I tried numerous examples, and perhaps knowing what i know now some of those would in fact work, but the approach i went with was indeed to implement a pipe and the code i used was that the posted by t.888
transform(obj: {[key: string]: any}, arg: string) {
if (!obj)
return undefined;
return arg === 'keyval' ?
Object.keys(obj).map((key) => ({ 'key': key, 'value': obj[key] })) :
arg === 'key' ?
Object.keys(obj) :
arg === 'value' ?
Object.keys(obj).map(key => obj[key]) :
null;
Honestly i think one of the things that was getting me was the lack of error handling, by adding the 'return undefined' call i believe we are now allowing for non expected data to be sent to the pipe, which obviously was occurring in my case.
if you don't want to deal with argument to the pipe (and look i don't think it's necessary in most cases) you can just return the following
if (!obj)
return undefined;
return Object.keys(obj);
Some Notes on creating your pipe and page or component that uses that pipe
is i was receiving errors about ‘name_of_my_pipe’ not being found
Use the ‘ionic generate pipe’ command from the CLI to ensure the pipe modules.ts are created and referenced correctly. ensure you add the following to the mypage.module.ts page.
import { PipesModule } from ‘…/…/pipes/pipes.module’;
(not sure if this changes if you also have your own custom_module, you may also need to add it to the custommodule.module.ts)
if you used the 'ionic generate page' command to make your page, but decide to use that page as your main page, remember to remove the page reference from app.module.ts (here's another answer i posted dealing with that https://forum.ionicframework.com/t/solved-pipe-not-found-in-custom-component/95179/13?u=dreaser
In my searching for answers there where a number of ways to display the data in the html file, and i don't understand enough to explain the differences. You may find it better to use one over another in certain scenarios.
<ion-item *ngFor="let myPost of posts">
<img src="https://somwhereOnTheInternet/{{myPost.ImageUrl}}"/>
<img src="https://somwhereOnTheInternet/{{posts[myPost].ImageUrl}}"/>
<img [src]="'https://somwhereOnTheInternet/' + myPost.ImageUrl" />
</ion-item>
However what worked that allowed me to display both the value and the key was the following:
<ion-list>
<ion-item *ngFor="let myPost of posts | name_of_pip:'optional_Str_Varible'">
<h2>Key Value = {{posts[myPost]}}
<h2>Key Name = {{myPost}} </h2>
</ion-item>
</ion-list>
to make the API call it looks like you need to import HttpModule into app.module.ts
import { HttpModule } from '#angular/http';
.
.
imports: [
BrowserModule,
HttpModule,
and you need Http in the page you make the call from
import {Http} from '#angular/http';
When making the API call you seem to be able to get to the children data (the objects or arrays within the array) 2 different ways, either seem to work
either during the call
this.http.get('https://SomeWebsiteWithAPI').map(res => res.json().anyChildren.OrSubChildren).subscribe(
myData => {
or when you assign the data to your local variable
posts: Array<String>;
this.posts = myData['anyChildren'];
(not sure if that variable needs to be an Array String, but thats what i have it at now. It may work as a more generic variable)
And final note, it was not necessary to use the inbuilt JSON library
however you may find these 2 calls handy for converting from an object to a string and vica versa
var stringifiedData = JSON.stringify(this.movies);
console.log("**mResults in Stringify");
console.log(stringifiedData);
var mResults = JSON.parse(<string>stringifiedData);
console.log("**mResults in a JSON");
console.log(mResults);
I hope this compilation of info helps someone out.
The dictionary is an object, not an array. I believe ng-repeat requires an array in Angular 2.
The simplest solution would be to create a pipe/filter that converts the object to an array on the fly. That said, you probably want to use an array as #basarat says.
If you have es6-shim or your tsconfig.json target es6, you could use ES6 Map to make it.
var myDict = new Map();
myDict.set('key1','value1');
myDict.set('key2','value2');
<div *ngFor="let keyVal of myDict.entries()">
key:{{keyVal[0]}}, val:{{keyVal[1]}}
</div>
In JavaScript this will translate to an object that with data might look like this
Interfaces in TypeScript are a dev time construct (purely for tooling ... 0 runtime impact). You should write the same TypeScript as your JavaScript.
Define the MapValuesPipe and implement PipeTransform:
import {Pipe, PipeTransform} from '#angular/core';
#Pipe({name: 'mapValuesPipe'})
export class MapValuesPipe implements PipeTransform {
transform(value: any, args?: any[]): Object[] {
let mArray:
value.forEach((key, val) => {
mArray.push({
mKey: key,
mValue: val
});
});
return mArray;
}
}
Add your pipe in your pipes module. This is important if you need to use the same pipe in more than one components:
#NgModule({
imports: [
CommonModule
],
exports: [
...
MapValuesPipe
],
declarations: [..., MapValuesPipe, ...]
})
export class PipesAggrModule {}
Then simply use the pipe in your html with *ngFor:
<tr *ngFor="let attribute of mMap | mapValuesPipe">
Remember, you will need to declare your PipesModule in the component where you want to use the pipe:
#NgModule({
imports: [
CommonModule,
PipesAggrModule
],
...
}
export class MyModule {}
So I was going to implement my own helper function, objLength(obj), which returns just Object(obj).keys.length. But then when I was adding it to my template *ngIf function, my IDE suggested objectKeys(). I tried it, and it worked. Following it to its declaration, it appears to be offered by lib.es5.d.ts, so there you go!
Here's how I implemented it (I have a custom object that uses server-side generated keys as an index for files I've uploaded):
<div *ngIf="fileList !== undefined && objectKeys(fileList).length > 0">
<h6>Attached Files</h6>
<table cellpadding="0" cellspacing="0">
<tr *ngFor="let file of fileList | keyvalue">
<td>{{file.value['fileName']}}</td>
<td class="actions">
<a title="Delete File" (click)="deleteAFile(file.key);">
</a>
</td>
</tr>
</table>
</div>
There's another way to loop over objects, using structural directives:
I prefer this approach because it "feels" most like the normal ngFor loop. :-)
(In this case for example I added Angular's context variables let i = index | even | odd | first | last | count) that are accessible inside my loop).
#Directive({
selector: '[ngForObj]'
})
export class NgForObjDirective implements OnChanges {
#Input() ngForObjOf: { [key: string]: any };
constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) { }
ngOnChanges(changes: SimpleChanges): void {
if (changes.ngForObjOf && changes.ngForObjOf.currentValue) {
// remove all views
this.viewContainerRef.clear();
// create a new view for each property
const propertyNames = Object.keys(changes.ngForObjOf.currentValue);
const count = propertyNames.length;
propertyNames.forEach((key: string, index: number) => {
const even = ((index % 2) === 0);
const odd = !even;
const first = (index === 0);
const last = index === (count - 1);
this.viewContainerRef.createEmbeddedView(this.templateRef, {
$implicit: changes.ngForObjOf.currentValue[key],
index,
even,
odd,
count,
first,
last
});
});
}
}
}
Usage in your template:
<ng-container *ngForObj="let item of myObject; let i = index"> ... </ng-container>
And if you want to loop using an integer value, you can use this directive:
#Directive({
selector: '[ngForInt]'
})
export class NgForToDirective implements OnChanges {
#Input() ngForIntTo: number;
constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.ngForIntTo && changes.ngForIntTo.currentValue) {
// remove all views
this.viewContainerRef.clear();
let currentValue = parseInt(changes.ngForIntTo.currentValue);
for (let index = 0; index < currentValue; index++) {
this.viewContainerRef.createEmbeddedView(this.templateRef, {
$implicit: index,
index
});
}
}
}
}
Usage in your template (example: loop from 0 to 14 (= 15 iterations):
<ng-container *ngForInt="let x to 15"> ... </ng-container>

Categories

Resources