I'm wrapping "web-ifc-viewer" in an angular application.
I've some troubles to hide and show components inside the IFC.
I've started from this example but I need to build a generic BIM viewer, so I can't define any category to prior.
import {AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '#angular/core';
import {IfcViewerAPI} from "web-ifc-viewer";
import {Subject, takeUntil} from "rxjs";
import {AppThemes, BimViewerService} from "./bim-viewer.service";
#Component({
selector: 'almaviva-bim-viewer',
templateUrl: './bim-viewer.template.html',
styleUrls: ['./bim-viewer.component.scss']
})
export class BimViewerComponent implements AfterViewInit {
#ViewChild('viewerContainer')
container!: ElementRef;
viewer?: IfcViewerAPI;
model: any;
ifcElement: any;
loadingValue: number = 0;
constructor() {
}
ngAfterViewInit(): void {
if (this.container) {
this.viewer = new IfcViewerAPI({container: this.container.nativeElement});
this.loadIfc('/assets/sample.ifc');
}
}
private async loadIfc(url: string) {
try {
if (this.viewer) {
await this.viewer.IFC.loader.ifcManager.useWebWorkers(true, '/assets/IFCWorker.js');
await this.viewer.IFC.setWasmPath("wasm/");
this.viewer.axes.setAxes(1000);
this.viewer.grid.setGrid(1000);
await this.viewer.IFC.loader.ifcManager.applyWebIfcConfig({
USE_FAST_BOOLS: true,
COORDINATE_TO_ORIGIN: true
});
this.viewer.IFC.loader.ifcManager.setOnProgress(
(event) => {
this.loadingValue = Math.floor((event.loaded * 100) / event.total);
}
)
this.model = await this.viewer.IFC.loadIfcUrl(url);
const project = await this.viewer.IFC.getSpatialStructure(this.model.modelID, true);
this.ifcElement = project.children[0];
await this.viewer.shadowDropper.renderShadow(this.model.modelID);
}
} catch (e) {
console.log(e);
}
}
async toggleLayer(event: Event, layer: any) {
const subset = this.viewer?.IFC.loader.ifcManager.createSubset({
modelID: this.model.modelID,
ids: [layer.expressID],
removePrevious: true,
customID: `${layer.expressID}-custom-id`
});
if (subset) {
this.viewer?.context.getScene().remove(subset);
}
}
}
When I toggle the layer (toggleLayer()) I receive an object from the subset like this
This is my html
<div>
<mat-sidenav-container>
<mat-sidenav mode="side" opened>
<mat-toolbar>
<span>
BIM
</span>
</mat-toolbar>
<mat-progress-bar mode="determinate" [value]="loadingValue"></mat-progress-bar>
<mat-list role="list" *ngIf="!ifcElement">
<mat-list-item role="listitem">
Caricamento IFC in corso...
</mat-list-item>
</mat-list>
<mat-accordion *ngIf="ifcElement">
<mat-expansion-panel *ngFor="let arch of ifcElement?.children || []">
<mat-expansion-panel-header>
<mat-panel-title>
{{arch.Name?.value || arch.LongName?.value || 'Architettura'}}
</mat-panel-title>
</mat-expansion-panel-header>
<mat-list role="list">
<mat-list-item role="listitem" *ngFor="let layer of arch.children">
<mat-checkbox (click)="toggleLayer($event, layer)">
{{layer.Name?.value || layer.LongName?.value || 'N/A'}}
</mat-checkbox>
</mat-list-item>
</mat-list>
</mat-expansion-panel>
</mat-accordion>
</mat-sidenav>
<mat-sidenav-content>
<div id="viewer-container" #viewerContainer></div>
<div class="loading-spinner-wrapper" *ngIf="loadingValue!==100">
<mat-spinner mode="indeterminate" diameter="35"></mat-spinner>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
Here the final result in the browser
The issue it's that nothing happens when I toggle the layer. And the subset log looks always the same.
When you load an IFC model, it also gets added to the scene. You need to remove the original model from the scene.
Instead of creating a subset every time the user toggles a layer, you need to create subsets for each layer in the beginning, and just add or remove the subset from the scene when the user toggles a layer. You can find a tutorial about that here, the full example here and the working app here.
The structure of your logic should be similar to the following snippet. This has been extracted from the minimal example linked above, so probably you need to adapt it a bit to you project:
import {
IFCWALLSTANDARDCASE,
IFCSLAB,
IFCDOOR,
IFCWINDOW,
IFCFURNISHINGELEMENT,
IFCMEMBER,
IFCPLATE,
} from 'web-ifc';
//Sets up the IFC loading
const ifcModels = [];
const ifcLoader = new IFCLoader();
ifcLoader.ifcManager.setWasmPath('../../../');
ifcLoader.load('../../../IFC/01.ifc', async (ifcModel) => {
ifcModels.push(ifcModel);
await setupAllCategories();
});
// Sets up optimized picking
ifcLoader.ifcManager.setupThreeMeshBVH(
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast);
// List of categories names
const categories = {
IFCWALLSTANDARDCASE,
IFCSLAB,
IFCFURNISHINGELEMENT,
IFCDOOR,
IFCWINDOW,
IFCPLATE,
IFCMEMBER,
};
// Gets the name of a category
function getName(category) {
const names = Object.keys(categories);
return names.find(name => categories[name] === category);
}
// Gets all the items of a category
async function getAll(category) {
return ifcLoader.ifcManager.getAllItemsOfType(0, category, false);
}
// Creates a new subset containing all elements of a category
async function newSubsetOfType(category) {
const ids = await getAll(category);
return ifcLoader.ifcManager.createSubset({
modelID: 0,
scene,
ids,
removePrevious: true,
customID: category.toString(),
});
}
// Stores the created subsets
const subsets = {};
async function setupAllCategories() {
const allCategories = Object.values(categories);
for (let i = 0; i < allCategories.length; i++) {
const category = allCategories[i];
await setupCategory(category);
}
}
// Creates a new subset and configures the checkbox
async function setupCategory(category) {
subsets[category] = await newSubsetOfType(category);
setupCheckBox(category);
}
// Sets up the checkbox event to hide / show elements
function setupCheckBox(category) {
const name = getName(category);
const checkBox = document.getElementById(name);
checkBox.addEventListener('change', (event) => {
const checked = event.target.checked;
const subset = subsets[category];
if (checked) scene.add(subset);
else subset.removeFromParent();
});
}
Subsets or createSubset() requires customID along side ids to identify the created subset from the original model without the customID you are overwriting your subset, it can be any string, in the example its was defined by category.toString()
I have been dealing with this scenario for a while, I appreciate your advice in advance
ngOnChanges runs in a context that I understand it shouldn't run. When modifying a property of a class which was initially set through #Input. This modification causes ngOnchanges hook to be executed in one context and not in another. I describe my scenario below
I have the following parent component that contains a list of customers that is passed to a child component,
Parent controller
export class AppComponent {
customers: ICustomer[];
currentlySelected: Option = 'All';
constructor() {
this.customers = [
{
id: 1,
name: 'Task1',
status: 'Pending',
},
{
id: 2,
name: 'Task2',
status: 'Pending',
},
{
id: 3,
name: 'Task3',
status: 'Progress',
},
{
id: 4,
name: 'Task4',
status: 'Closed',
},
];
}
selectBy(option: Option): void {
this.currentlySelected = option;
}
filterBy(): ICustomer[] {
if (this.currentlySelected === 'All') {
return this.customers;
}
return this.customers.filter(
(customer) => customer.status === this.currentlySelected
);
}
}
Parent template
<nav>
<ul>
<li (click)="selectBy('All')">All</li>
<li (click)="selectBy('Pending')">Pending</li>
<li (click)="selectBy('Progress')">Progress</li>
<li (click)="selectBy('Closed')">Closed</li>
</ul>
</nav>
<app-list [customers]="filterBy()"></app-list>
Before passing customer to the child component they are filtered according to the customer status property, that is the purpose of the filterBy function.
The child component in the hook ngOnChanges modifies each customer by adding the showDetail property and assigns it the value false
export class ListComponent implements OnInit, OnChanges {
#Input() customers: ICustomer[] = [];
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
this.customers = changes.customers.currentValue.map(
(customer: ICustomer) => ({
...customer,
showDetail: false,
})
);
}
ngOnInit(): void {
console.log('init');
}
toggleDetail(current: ICustomer): void {
this.customers = this.customers.map((customer: ICustomer) =>
customer.id === current.id
? { ...customer, showDetail: !customer.showDetail }
: { ...customer }
);
}
}
Calling the toggleDetail method changes the value of the showDetail property to show the customer's detail
child template
<table>
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let customer of customers">
<tr>
<td>{{ customer.name }}</td>
<td>
<button (click)="toggleDetail(customer)">Detail</button>
</td>
</tr>
<tr *ngIf="customer.showDetail">
<td colspan="2">
<pre>
{{ customer | json }}
</pre>
</td>
</tr>
</ng-container>
</tbody>
</table>
The behavior that occurs is the following, when all clients are listed and click on detail it works as expected, but if I change to another state and the list is updated and I click on detail it does not show the detail. The reason is that the ngOnchanges hook is re-executed causing the showDetail property to be set to false again, thus defeating my intention.
Why is ngOnChanges executed in this context? What alternative is there to solve it?
Update 1
I have added sample app: https://stackblitz.com/edit/angular-ivy-dkvvgt?file=src/list/list.component.html
You has in your code
<app-list [customers]="filterBy()"></app-list>
Angular can not know the result of the function "filterBy" until executed, so it's executed each time check. This is the reason we should avoid use functions in our .html (We can use, of course, but we need take account this has a penalty). Use an auxiliar variable
customerFilter:ICustomer[];
In constructor you add
constructor() {
this.customers = [...]
this.customerFilter=this.customers //<--add this line
}
And in selectBy
selectBy(option: Option): void {
this.currentlySelected = option;
this.customerFilter=this.filterBy() //<--this line
}
Now pass as argument the customerFilter
<app-list [customers]="customerFilter"></app-list>
Your forked stackblitz
Angular runs ngOnChanges when any of the inputs change. When you use an object as an input parameter Angular compares references. As Eliseo said Angular calls your filterBy function on each change detection, and it's not a problem when the currentlySelected is All, beacuse you return the same array reference and it won't trigger change detection in your list component. However when it's not, that causes an issue. You filter your array on each change detection and that results in a new array every time. Now Angular detects that the #Input() changed and runs ngOnChanges.
You can do as Eliseo said, that's a solution too. My suggestion is to create a pipe, it's makes the component.ts less bloated.
#Pipe({
name: 'filterCustomers',
})
export class FilterCustomersPipe implements PipeTransform {
transform(customers: ICustomer[] | null | undefined, filter: Option | undefined | undefined): ICustomer[] | undefined {
if (!customers) {
return undefined;
}
if (!filter || filter === 'All') {
return customers;
}
return customers.filter((customer) => customer.status === filter);
}
}
I prefere writing out null | undefined too, so it's safer with strictTemplates.
You can use this pipe like this:
<app-list [customers]="customers | filterCustomers : currentlySelected"></app-list>
Here you can read more about Angular pipes.
Another suggestion:
Your nav doesn't have button elements, you bind your (click) events on li elements. That's a really bad practice as it not focusable by keyboard. More about HTML Accessibility.
I am building my own custom WYSIWYG text editor in Angular using a content editable div. I am extracting it as a component. I want to be able to type some text and format it and then when I click a button from the parent component it should either change the text in the editor or update it.
My parent component looks as follows:
#Component({
selector: 'app-parent',
templateUrl: `
<form [formGroup]="form">
<app-default-editor
formControlName="body" >
</app-default-editor>
</form>
<button (click)="changeValue()">Change Value</button>
<p *ngIf="!form.valid">Counter is invalid!</p>
<pre>{{ form.value | json }}</pre>
`
})
export class ParentComponent {
initialText = '<H1>This is the heading</H1>';
form = this.fb.group({
body: '<H1>This is the heading</H1>'
});
changeValue(){
this.form.patchValue({ body: 'This is my new text' });
}
}
My editor component looks as follows:
#Component({
selector: 'app-parent',
templateUrl: `
<button (click)="executeCommand('bold')" ><i class="fas fa-bold"></i></button>
<button (click)="executeCommand('italic')" ><i class="fas fa-italic"></i></button>
<button (click)="executeCommand('underline')" ><i class="fas fa-underline"></i></button>
<div
#htmlEditor
id="htmlEditor"
contenteditable="true"
(input)="onTextChanged()"
[innerHTML]="initialText">
</div>
`
})
export class EditorComponent implements ControlValueAccessor, OnChanges {
#Input()
initialText = "";
#Input()
_textValue = "";
#Input()
minValue = 0;
#ViewChild('htmlEditor') htmlEditor;
ngAfterViewInit() {
this.htmlEditor.nativeElement.innerHTML = this.initialText;
}
get textValue() {
return this._textValue;
}
set textValue(val) {
this._textValue = val;
this.propagateChange(this._textValue);
}
writeValue(value: any) {
if (value !== undefined) {
this.textValue = value;
}
}
propagateChange = (_: any) => { };
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched() { }
validateFn: Function;
ngOnChanges(changes) {
this.validateFn = createEditorRequiredValidator();
}
onTextChanged() {
this.textValue = this.htmlEditor.nativeElement.innerHTML;
}
executeCommand(cmd) {
document.execCommand(cmd, false, this.htmlEditor.nativeElement);
}
}
export function createEditorRequiredValidator() {
return function validateEditor(c: FormControl) {
let err = {
requiredError: {
given: c.value,
required: true
}
};
return (c.value.length == 0) ? err : null;
}
}
With the current code, I can format text to be bold, italic etc and the form in the parent component can display the value in the tags. When I click the button to change the text to a predefined value the form updates but the editor doesn't show any changes.
There are two little mistakes that prevent your code from working as it should.
1. No provider for NG_VALUE_ACCESSOR
This one should have given you errors in your console. To make sure you can use formControlName directive the component needs to provide a value accessor. You achive that by adding the providers when defining the editor component. Like this:
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EditorComponent), multi: true}
]
When you did this the writeValue function within your editor component is called everytime the form control for the field body changes. Lets go to little mistake number two...
2. innerHtml bound to initialText
You update the text property within writeValue but that property is never bound to the template. The innerHtml binding only looks for the initialText property - and that hasn´t changed.
Change your binding to look like this:
<div #htmlEditor
id="htmlEditor"
contenteditable="true"
(input)="onTextChanged()"
[innerHTML]="textValue">
You can utilize the initial text within the getter and return it in case of an empty text value.
I ended up changing the changeValue method in the parent component to:
changeValue(){
this.form.patchValue({ body: [WHATEVER THE TEXT MUST BE] });
this.initialText = [WHATEVER THE TEXT MUST BE]
}
This seems to work but I don't think it is the best answer.
I have a page with three components:
1. Products list component which gets some products as input and display them.
2. Filters component which displays some filters list i.e. (size, colour,...) and also display the added filters.
3. Main component which is the root component
Let say a user adds 1 filter which fires a http request to get new filtered products and while the request is pending he removes the added filter which fires another http request to fetch all the products
How to cancel the first request so we don't display the filtered products?
Here is my code:
class FiltersService {
private _filters: any[];
get filters() {
return this._filters;
}
addFilter(filter) {
this._filters.push(filter);
}
removeFilter(filter) {
// Remove filter logic ...
}
}
class DataService_ {
constructor(private http: HttpClient) {
}
getProducts(filters) {
return this.http.post<any[]>('api/get-products', filters)
}
}
#Component({
selector: 'app-main',
template: `
<div>
<app-filters [filtersChanged]="onFiltersChange()"></app-filters>
<app-products-list [products]="products"> </app-products-list>
</div>
`
})
class MainComponent {
products: any[];
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
let filters = this.filtersService.filters;
this.dataService.getProducts(filters)
.subscribe(products => this.products = products)
}
onFiltersChange() {
this.setProducts();
}
}
#Component({
selector: 'app-filters',
template: `
<div>
Filters :
<ul>
<li *ngFor="let filter of filters" (click)="addFilter(filter)"> {{ filter.name }}</li>
</ul>
<hr>
Added Filters:
<ul>
<li *ngFor="let filter of filtersService.filters"> {{ filter.name }} <button (click)="removeFilter(filter)"> Remove</button></li>
</ul>
</div>
`
})
class FiltersComponent {
filters = [{ name: 'L', tag: 'size' }, { name: 'M', tag: 'size' }, { name: 'White', tag: 'colour' }, { name: 'Black', tag: 'colour' }]
#Output() filtersChanged = new EventEmitter()
constructor(public filtersService: FiltersService) {
}
addFilter(filter) {
const isAdded = this.filtersService.filters.find(x => x.name === filter.name);
if (isAdded) return;
this.filtersService.addFilter(filter);
this.filtersChanged.emit()
}
removeFilter(filter) {
this.filtersService.remove(filter);
this.filtersChanged.emit()
}
}
#Component({
selector: 'app-products-list',
template: `
<div>
<h1>Products</h1>
<ul *ngIf="products.length">
<li *ngFor="let product of products">
{{product.name }}
</li>
</ul>
</div>
`
})
class ProductsListComponent {
#Input() products
constructor() {
}
}
Long story short:
Easiest way to handle such situations is by using the switchMap operator. What this does is cancel the internal subscription as soon as a new event comes along.
One implementation would be:
class MainComponent {
products: any[];
private _filters$ = new Subject();
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
this._filters$
.switchMap((filters)=> this.dataService.getProducts(filters)) // or .let(switchMap...) if you are using rxjs >5.5
.subscribe(products => this.products = products);
}
onFiltersChange() {
this._filters$.next(this.filtersService.filters);
}
}
Long story:
What happens here is:
When you change filter the onFilterChange is triggered. You then emit the latest filters (inside this.filtersService.filters) through the _filters$ Subject (a subject is almost identical to an EventEmitter).
Back in time during component initialization the ngOnInit method has called setProducts, which has subscribed to the _filters$ subject for future events (none has happened at this point). When an event arrives on _filters$ then we trigger the getProducts method of dataservice, passing it the filters that where contained in the event. We will be waiting on this line until the http call has completed. As soon as it completes the result of the http call will be assigned to the products of the component.
If while we are waiting for the http response to get back, onFiltersChange is fired again, then a new event will arive at the switchMap and it will cancel the previous http request so that it can handle the new event.
This is a very powerful approach as changing a single operator, you can easily change the behavior of your app. For instance, changing switchMap to concatMap will make the request wait for the previous one to complete (will happen serially). Changing it to flatMap will have the same behaviour as the original code you posted (http requests will happen as soon as filters change, without affecting previous ones, order of responses will not predictable) and so on.
Note : to cancel the request just use unsubscribe.
For exmple
const course$ = this.service$.getCourses(`/api/courses`).subscribe(courses => { console.log(courses) }
setTimeout(() => course$.unsubscribe(),1000) // cancel the request
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>