Iterating over n object of objects using ngFor recursivley - html

I have an Angular Project where I need to show Folder structures of a Server.
I have json data in this form(demo names):
{
"dirPath": "file:///C:/dev/gallery/filesFromApp/",
"folderName": "filesFromApp",
"fileNames": [
"firmwareUpdate101.txt",
"keyboard_output.txt",
"screen-locks.txt"
],
"dirInfoList": [
{
"dirPath": "file:///C:/dev/gallery/filesFromApp/Beta1/",
"folderName": "Beta1",
"fileNames": [
"beta1Ubdate.txt"
],
"dirInfoList": []
},
{
"dirPath": "file:///C:/dev/gallery/filesFromApp/Beta2/",
"folderName": "Beta2",
"fileNames": [
"Beta2UPDATE!.txt"
],
"dirInfoList": [
{
"dirPath": "file:///C:/dev/gallery/filesFromApp/Beta2/omaewa/",
"folderName": "omaewa",
"fileNames": [
"nani.txt"
],
"dirInfoList": []
}
]
}
]
}
What I need to do is show every Folder in an Angular Material Expansion Panel:
<mat-expansion-panel *ngIf="dirInfos !== undefined">
<mat-expansion-panel-header>
<mat-panel-title>
{{dirInfos.folderName}}
</mat-panel-title>
<mat-panel-description>
{{dirInfos.dirPath}}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-expansion-panel *ngFor="let dir of dirInfos.dirInfoList">
<mat-expansion-panel-header>
<mat-panel-title>
{{dir.folderName}}
</mat-panel-title>
<mat-panel-description>
{{dir.dirPath}}
</mat-panel-description>
</mat-expansion-panel-header>
</mat-expansion-panel>
<p *ngFor="let file of dirInfos.fileNames">{{file}}</p>
</mat-expansion-panel>
Image to Visual example
My Problem is that the File structure can go very deep, i.e 25 Folders in. I have no idea how to recursively check this, and then generate the needed HTML, any Ideas? ngFor might be helpful but I am not aware of a way to make this work, even if I use Object.Keys on the objects.

You can use the recursive template design pattern for your use case. like in here.
demo

you can iterate loop by using a keyvalue pipe. so you can be able to iterate loop over the object.
so the logic here we implement is the when you have object in json use keyvalue pipe when it is array use simple *ngFor with out keyvalue pipe.
<mat-expansion-panel *ngIf="dirInfos !== undefined">
<mat-expansion-panel-header>
<mat-panel-title>
{{dirInfos.folderName}}
</mat-panel-title>
<mat-panel-description>
{{dirInfos.dirPath}}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-expansion-panel *ngFor="let dir of dirInfos.dirInfoList | keyvalue">
<mat-expansion-panel-header>
<mat-panel-title>
{{dir | json}}
<!-- HERE YOU WILL GET THE DATA WHICH IS SEPARATED INTO IN TO KEY VALUE PAIR -->
</mat-panel-title>
<mat-panel-description>
{{dir.dirPath}}
</mat-panel-description>
</mat-expansion-panel-header>
</mat-expansion-panel>
<p *ngFor="let file of dirInfos.fileNames">{{file}}</p>
</mat-expansion-panel>

Related

Get data from nested json in Angular

Im trying to iterate over an array but page is displaying [ object Object] instead. How can I get data.id of my example?
Service:
return this._http.get<CustomersList[]>(this.apiUrl,{ headers: reqHeader });
My json looks like this:
[ {"current_page":1,"data":[{"id":25},{"id":26}] }]
And here is my component:
customerslist$: CustomersList[];
return this.customerdataService.getCustomersList().subscribe(data => this.customerslist$ = data);
Would be great if anybody can help me?
The data property is an array contained in an array. You either need two *ngFor directives
<ng-container *ngFor="let customer of customerslist$">
<ng-container *ngFor="let item of customer.data">
{{ item.id }}
</ng-container>
/ng-container>
Or if you're sure the customerslist$ array will always contain only one element, you could access the first element directly.
<ng-container *ngFor="let item of customerslist$[0].data">
{{ item.id }}
</ng-container>
Also if you're aren't using the this.customerList$ in the controller, you could skip the subscription in the controller and use the async pipe in the template. It takes care of any potential memory leak issues due to open subscriptions.
Contoller
customerslist$: Observable<CustomersList[]>; // <-- typeof `Observable`
ngOnInit() {
this.customerslist$ = this.customerdataService.getCustomersList(); // <-- don't subscribe yet
}
<ng-container *ngIf="(customerslist$ | async) as customerslist">
<ng-container *ngFor="let customer of customerslist">
<ng-container *ngFor="let item of customer.data">
{{ item.id }}
</ng-container>
</ng-container>
</ng-container>
By convention, variable names suffixed with a dollar sign (customerlist$) are used to denote observables.
If you are using Observable $ sign at the end of variable denotes observable,
in that case you do not need to subscribe only return value, in template you can subscribe using async pipe which automatically unsubscribe as well to prevent memory leak and a recommended approach.
Here we assume we are only taking first element of array that why directly accessing it giving first index, otherwise we need to iterate that array in template as well.
customerslist$: Observable<CustomersList[]>;
this.customerslist$ = this.customerdataService.getCustomersList()
.pipe(map(res) => res[0].data));
In template use async pipe to subscribe
<ng-container *ngFor="let data of customerslist$ | async">
<span> {{ data.id }} </span>
</ng-container>
WITHOUT OBSERVABLE
customerslist: CustomersList[];
this.customerdataService.getCustomersList()
.subscribe((res) => this.customerslist = res[0].data));
In template
<ng-container *ngFor="let data of customerslist">
<span> {{ data.id }} </span>
</ng-container>

How i can do a *ngFor in a array of objects that i don't know the name property?

I have this array called dadosRelatorio that i need to iterate in my template, this array i get in a http request:
[{"id": 1,
"name": "a",
"class":[{
"Inglês":[{
"id": 1,
"code": "teste"
},
{
"id": 2,
"code": "teste 2"
}],
"Espanhol":[{
"id": 1,
"code": "a"
}]
}]
}]
How i can iterate the objects inside class if they have different names property? Sometimes the name can be "Inglês" and sometimes can be "Espanhol" and other names...
When i know the name of the property i do this way:
<div *ngFor="let aluno of dadosRelatorio" class="div-aluno">
<span>Aluno: {{ aluno.name }}</span>
<div *ngFor="let classe of aluno.class">
<div *ngFor="let idioma of classe. ???>
</div>
</div>
</div>
But how i can iterate the object inside class?
You may iterate using the keyvalue pipe as shown below:
<div *ngFor="let aluno of dadosRelatorio" class="div-aluno">
<span>Aluno: {{ aluno.name }}</span>
<div *ngFor="let classe of aluno.class | keyvalue">
<div> {{classe.key}}</div>
<div *ngFor="let type of classe.value | keyvalue">
{{type.key}}
<div *ngFor="let data of type.value | keyvalue">
{{data.value.id }}
{{data.value.code }}
</div>
</div>
</div>
</div>
You can enumerate an object keys using Object.keys.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys.
edit: You can also use the keyvalue pipe as describe in the official documentation: https://angular.io/api/common/KeyValuePipe
I thought there was a ngForIn directive as well, but I cant find the doc.

Reference ngFor value in component

I have a nested ngFor statement. I need to retrieve the value of my first ngFor on button click.
I have tried the following:
use template reference variable
use attribute binding
use Input decorator
This is my code:
<mat-expansion-panel *ngFor="let item of Datasource;">
<mat-expansion-panel-header style="display:flex" class="mat-row">
{{item.Header}}
</mat-expansion-panel-header>
<mat-selection-list [(ngModel)]="selectedOptions">
<mat-list-option *ngFor="let line of item.match; let i= index;" [value]="line">
<div class="container-name">
<div class="col-6">{{i}} - {line.user.Name }} vs {{ line.user.Address }}</div>
</mat-list-option>
</mat-selection-list>
<div style="text-align:center; padding: 20px">
<button mat-raised-button color="primary" (click)="submit()" type="submit">Add</button>
</div>
</mat-expansion-panel>
Can this be achieved?
Well, you need to clone that object properties first. As that object is linked to the template, when you manipulate it, it is manipulated on template too. You can use var obj = Object.assign({}, actual obj) and then do the manipulation on obj instead of actual one. Then it will not get affected in template. Hope it helps.

How to toggle mat-expansion-panel with button click?

Is there any way in which I can expand a particular mat-expansion-panel by clicking an external button?
I have tried linking to the ID of the panel, but with no success...
<mat-expansion-panel id="panel1"> ... </>
...
<button (click)="document.getElementById('panel1').toggle()>Click me</button>
Here is my stackblitz code for example
My eventual plan is to use this method to open different panels within a list generated from an array: <mat-expansion-panel *ngFor="let d of data"> ...
In your html file:
<mat-expansion-panel [expanded]="panelOpenState">
<mat-expansion-panel-header>
<mat-panel-title>
TITLE
</mat-panel-title>
</mat-expansion-panel-header>
<p>BODY</p>
</mat-expansion-panel>
<button mat-raised-button (click)="togglePanel">TOGGLE</button>
In your TS file:
panelOpenState: boolean = false;
togglePanel() {
this.panelOpenState = !this.panelOpenState
}
If you use *ngFor to generate the expansion panels:
<mat-expansion-panel [expanded]="isOpen" *ngFor="let d of data">
<mat-expansion-panel-header>
{{ d.header }}
</mat-expansion-panel-header>
<p>{{ d.content }}</p>
</mat-expansion-panel>
<button mat-raised-button (click)="togglePanel">TOGGLE</button>
If you press the button all of the expanded panels opens
simultaneously.
To open only one panel with one button, add a "expanded" property to the data array for each element like this:
data = [
{id:1, header:'HEADER 1', content:'CONTENT 1', expanded: false},
{id:2, header:'HEADER 2', content:'CONTENT 2', expanded: false},
{id:3, header:'HEADER 3', content:'CONTENT 3', expanded: false},
{id:4, header:'HEADER 4', content:'CONTENT 4', expanded: false},
]
Then in your template:
<mat-expansion-panel [(ngModel)]="d.expanded"
[expanded]="d.expanded" *ngFor="let d of data" ngDefaultControl>
<mat-expansion-panel-header>
<button (click)="toggle(d.expanded)">TOGGLE</button>
{{ d.header }}
</mat-expansion-panel-header>
<p>{{ d.content }}</p>
</mat-expansion-panel>
And the method raised by the button click:
toggle(expanded) {
expanded = !expanded;
}
<mat-expansion-panel [disabled]="true"
#mep="matExpansionPanel"
*ngFor="let foo of list">
<mat-expansion-panel-header>
<button (click)="mep.expanded = !mep.expanded">Toggle</button>
</mat-expansion-panel-header>
<p>Text</p>
</mat-expansion-panel>
Use two-way binding on the expanded attribute of mat-expansion-panel. Here is an example live in StackBlitz:
https://stackblitz.com/edit/angular-gtsqg8
<button (click)='xpandStatus=xpandStatus?false:true'>Toggle it</button>
<p>
<mat-expansion-panel [(expanded)]="xpandStatus">
<mat-expansion-panel-header>
<mat-panel-title>This an expansion panel</mat-panel-title>
<mat-panel-description>xpandStatus is {{xpandStatus}}</mat-panel-description>
</mat-expansion-panel-header>
Two-way binding on the expanded attribute gives us a way to store and manipulate the expansion status.
</mat-expansion-panel>
</p>
You can use the method toggle().
First give the element an id.
<mat-expansion-panel #matExpansionPanel>
Next, access the element from javascript. Import necessary libraries (MatExpansionPanel, ViewChild)
#ViewChild(MatExpansionPanel, {static: true}) matExpansionPanelElement: MatExpansionPanel;
Lastly, call the toggle function
this.matExpansionPanelElement.toggle(); //open(), close()
<mat-nav-list>
<mat-expansion-panel *ngFor="let row of rows" #mep="matExpansionPanel">
<mat-expansion-panel-header>
{{row}}
</mat-expansion-panel-header>
<h2>Test</h2>
<button (click)="mep.toggle()">Toggle</button>
</mat-expansion-panel>
</mat-nav-list>
Working Example:
https://stackblitz.com/edit/mat-expansion-panel-vymjsq?file=app%2Fexpansion-overview-example.html
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<mat-accordion displayMode="flat" multi class="mat-table">
<section matSort class="mat-elevation-z8 mat-header-row">
<span class="mat-header-cell" mat-sort-header="vesselName"></span>
<span class="mat-header-cell" mat-sort-header="vesselName">d</span>
</section>
<mat-expansion-panel [disabled]="true" #mep="matExpansionPanel"
*ngFor="let d of data">
<mat-expansion-panel-header>
<span class="mat-cell" (click)="mep.expanded = !mep.expanded">
<mat-icon class="icon" *ngIf="!mep.expanded">expand_more</mat-icon>
<mat-icon class="icon" *ngIf="mep.expanded">expand_less</mat-icon>
</span>
<span (click)="dClicked(d)" class="mat-cell">{{d.dataSet}}</span>
</mat-expansion-panel-header>
<div><pre>{{d | json}}</pre></div>
</mat-expansion-panel>
<div class="well" *ngIf="!d || d.length == 0">
<p>There are no d for this.</p>
</div>
</mat-accordion>
html:
<mat-accordion >
<mat-expansion-panel #first="matExpansionPanel">
<mat-expansion-panel-header>
<mat-panel-title>...</mat-panel-title>
</mat-expansion-panel-header>
...
</mat-expansion-panel>
<mat-expansion-panel #second="matExpansionPanel" expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>...</mat-panel-title>
</mat-expansion-panel-header>
...
</mat-expansion-panel>
</mat-accordion>
<button (click)="doSomething(first, second)">Click</button>
ts:
import { Component } from '#angular/core';
import { MatExpansionPanel } from '#angular/material';
#Component({
selector: 'app-home',
templateUrl: './home.component.html'
})
export class HomeComponent {
doSomething(first: MatExpansionPanel, second: MatExpansionPanel) {
if (first.expanded ) { // check if first panel is expanded
first.close(); // close first panel
second.open(); // open second panel
// ...
}
}
}
Read more

Can't use *ngIF inside *ngFor : angular2 [duplicate]

This question already has answers here:
*ngIf and *ngFor on same element causing error
(17 answers)
Closed 5 years ago.
i'm using angular2 and i'im binding data from a service , the probleme is when i'm loading data i should filter it by an id , , this is what i'm supposed to do :
<md-radio-button
*ngFor="#item of items_list"
*ngIf="item.id=1"
value="{{item.value}}" class="{{item.class}}" checked="{{item.checked}}"> {{item.label}}
</md-radio-button>
and this is the data:
[
{ "id": 1, "value": "Fenêtre" ,"class":"md-primary" ,"label":"Fenêtre" ,"checked":"true"},
{ "id": 2, "value": "Porte Fenêtre" ,"class":"" ,"label":"Porte Fenêtre" }
]
by the way i want just the data with id =1 to be accepted , but i'm seeing this error:
EXCEPTION: Template parse errors:
Parser Error: Bindings cannot contain assignments at column 14 in [ngIf item.id=1] in RadioFormesType1Component#10:16 ("
<md-radio-button
*ngFor="#item of items_list"
[ERROR ->]*ngIf="item.id=1"
value="{{item.value}}" class="{{item.class}}" checked="{{item.check"): RadioFormesType1Component#10:16
so any suggestion to use ngif and ngfor together ?
You could use the following:
*ngIf="item.id===1"
instead of
*ngIf="item.id=1"
You try to assign something into the id property (operator =) instead of testing its value (operator == or ===).
Moreoever both ngFor and ngIf on the same element aren't supported. You could try something like that:
<div *ngFor="#item of items_list">
<md-radio-button
*ngIf="item.id===1"
value="{{item.value}}" class="{{item.class}}"
checked="{{item.checked}}">
{{item.label}}
</md-radio-button>
</div>
or
<template ngFor #item [ngForOf]="items_list">
<md-radio-button
*ngIf="item.id===1"
value="{{item.value}}" class="{{item.class}}"
checked="{{item.checked}}">
{{item.label}}
</md-radio-button>
</template>
Another solution is to create custom filtering pipe:
import {Pipe} from 'angular2/core';
#Pipe({name: 'filter'})
export class FilterPipe {
transform(value, filters) {
var filter = function(obj, filters) {
return Object.keys(filters).every(prop => obj[prop] === filters[prop])
}
return value.filter(obj => filter(obj, filters[0]));
}
}
and use it like this in component:
<md-radio-button
*ngFor="#item of items_list | filter:{id: 1}"
value="{{item.value}}" class="{{item.class}}" checked="{{item.checked}}"> {{item.label}}
</md-radio-button>
Custom pipe needs to be registered on the component:
#Component({
// ...
pipes: [FilterPipe]
})
Demo: http://plnkr.co/edit/LK5DsaeYnqMdScQw2HGv?p=preview
*ngIf and *ngFor on the same tag are not supported. You need to use the long form with an explicit <template> tag for one of them.
update
Instead of <template> use <ng-container> which allows to use the same syntax as inline *ngIf and *ngFor
<ng-container *ngFor="#item of items_list">
<md-radio-button
*ngIf="item.id=1"
value="{{item.value}}" class="{{item.class}}" checked="{{item.checked}}"> {{item.label}}
</md-radio-button>
</ng-container>