Rendering nested objects using a recursive method - arrays

I have an somewhat complex object that includes nested objects as follows
"data": {
"John": {
"title": "John",
"value": "john"
},
"Ben": {
"title": "Ben",
"value": "ben"
},
"Workers": {
"title": "Working Data",
"startData": {
"title": "Start Date",
"value": "Mon, 27 Nov 2017 16:57:56 GMT"
},
"isPermanant": {
"title": "Is Permanant",
"value": "True"
}
},
"Family": {
"title": "Family Data",
"familyMembers": {
"title": "Family Members",
"value": "4"
},
"pets": {
"title": "Pets",
"value": "2"
}
},
"education": {
"title": "Education Details",
"degree": {
"title": "Degree",
"value": "Yes"
},
"graduated": {
"title": "Graduated Year",
"value": "2015"
}
}
Expected outcome is something like this
<p>John <span>john</span><p>
<p>Ben <span>ben</span><p>
<p>Working Data<p>
<p>Start Date <span>Mon, 27 Nov 2017 16:57:56 GMT</span><p>
<p>Is Permanant <span>True</span><p>
<p>Family Data<p>
<p>Family Members <span>4</span><p>
<p>Pets <span>2</span><p>
<p>Education Details<p>
<p>Degree <span>Yes</span><p>
<p>Graduated Year<span>2015</span><p>
I created a component that using a recursive way of displaying data
import { Component, Input, OnInit } from '#angular/core'
#Component({
selector: 'timeline-data',
template: 'timeline-data.html'
})
export class TimelineDataComponent implements OnInit {
#Input('data') data: any[];
ngOnInit() {}
}
timeline-data.html as follows
<ng-container *ngIf="data.length">
<ng-container *ngFor="let item of data">
<ng-container *ngIf="item.value">
<p>{{ item.title }} <span>{{ item.value }}</span></p>
</ng-container>
<ng-container *ngIf="!item.value">
<timeline-data [data]="[item]"></timeline-data>
</ng-container>
<ng-container>
<ng-container>
But when I run this angular give me a RangeError: Maximum call stack size exceeded
What am I doing wrong here? How should I show this? Thanks in advance.

Based on your sample html, it will be difficult to do recursive rendering and not end up with nested <p> tags.
I took a slightly different approach and used an unordered list with an extra conditional to eliminate the empty li from the first iteration. If someone has a better way to do this, I'm all ears :)
I broke the rendering up into two main components:
tree.component.ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'tree-component, [tree-component]',
template: `
// omit the <li> wrapper for the "parent" iteration
<ng-container *ngIf="parent">
<tree-item [data]="data"></tree-item>
</ng-container>
<li *ngIf="!parent">
<tree-item [data]="data"></tree-item>
</li>
`
})
export class TreeComponent {
#Input()
data: object;
#Input()
parent = false;
}
tree-item.component.ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'tree-item',
template: `
// iterate over the keys for each data item
<ng-container *ngFor="let key of data | keys; let i = index;">
// if the value is not an object, output the values
// I assumed there would only be two values to wrap the second
// value in a <span> according to your sample
<ng-container *ngIf="!isObject(data[key])">
<ng-container *ngIf="i === 0">{{ data[key] }}</ng-container>
<span *ngIf="i === 1">{{ data[key] }}</span>
</ng-container>
// if the value is an object, recursively render the tree-component
<ul tree-component *ngIf="isObject(data[key])" [data]="data[key]"></ul>
</ng-container>
`
})
export class TreeItemComponent {
#Input()
data: object;
isObject(value: any): boolean {
return value instanceof Object && value.constructor === Object;
}
}
and a pipe utility to get the keys for each object, keys.pipe.ts:
import { PipeTransform, Pipe } from '#angular/core';
#Pipe({
name: 'keys'
})
export class KeysPipe implements PipeTransform {
transform(value, args: string[]): any {
return Object.keys(value);
}
}
Give your data, and an implementation of:
<tree-component [data]="data" [parent]="true"></tree-component>
You end up with the result of this Plunkr: https://plnkr.co/edit/9DiCymkBDUNJSCFObV2G

Related

Data from 2 arrays

So i have two arrays:
1:
"movies":
{
"id": "123bb",
"category": "3345",
"content": "Sinister"
}
Second:
"categories":
{
"id": "3345",
"code": "Movie",
"name": "Horror"
},
I also have random movie:
TS:
loadData() {
this.PagesService.loadData().subscribe(response => {
console.log(response)
this.movies = response
this.movies[Math.floor(Math.random() * this.movies.length)];
this.randomValue = this.movies[Math.floor(Math.random() * this.movies.length)];
return this.randomValue
})
console.log(this.randomValue)
};
HTML:
<p>{{ this.randomValue.content }}</p>
So if I have something like this:
<p>{{ this.randomValue.category }}</p>
There is "3345" as category but I would like to have "name" from this second array but I am not sure how to do that
My recommendation:
store the data as observable
map the data in observable pipe (maybe use combineLatest to combine movies and categories data)
use the async pipe in template
use an own structural directive to pick the datas random element
Here is how it could look like...
The directive
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
interface RandomContext<T> {
appRandom: T[];
$implicit?: T;
}
#Directive({
standalone: true,
selector: '[appRandom]',
})
export class RandomDirective<T> {
private context: RandomContext<T> = {
appRandom: [],
$implicit: undefined,
};
#Input()
set appRandom(elements: T[]) {
this.context.appRandom = elements;
this.pickNewElement();
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {
this.viewContainer.createEmbeddedView(this.templateRef, this.context);
}
private pickNewElement(): void {
this.context.$implicit =
this.context.appRandom[
Math.floor(Math.random() * this.context.appRandom.length)
];
}
}
And your template:
<p *appRandom="movies$ | async; let randomMovie">
{{ randomMovie.category }}
</p>
And your component:
movies$: Observable<ExtendedMovie[]>;
ngOnInit() {
this.movies$ = combineLatest([
this.PagesService.loadData(),
this.CategoriesService.loadCategories(),
]).pipe(
map(([movies, categories]) => {
return /*map movies array to extend it with your required data from categories array*/;
}),
);
}

Angular TS2322: Type 'string' is not assignable to type 'IHamster[]'

i have some problems with my code, it looks like a simple one. Type string is not assignable to type Hamster[], but I can't find the issue 😞.
I can pass value to the child, but if I use the array of IHamster, I get this error. Hope someone can help me.
And sorry for the quality of question, its my first first post 🤖
MAIN
import { IHamster } from './ihamster';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title: string = "refresher"
hamsters: IHamster[] = [
{
img: "https://images.unsplash.com/photo-1533152162573-93ad94eb20f6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80",
alias: "Mr. Hamster",
name: "Hamsterz",
bio: "I am here to find some friends!"
},
{
img: "https://images.unsplash.com/photo-1625406736528-42c8d985a072?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2669&q=80",
alias: "Mrs. Hamster",
name: "Hammine",
bio: "I am here to see what my husband does here!"
},
{
img: "https://images.unsplash.com/photo-1621668590468-828e1344466b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80",
alias: "Bob",
name: "Hon Hamo",
bio: "I am here to hack ervery body ervery were, i got the red pill!"
},
{
img: "https://images.unsplash.com/photo-1618232118117-98d49b20e2f5?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80",
alias: "Charles",
name: "Chano Chai",
bio: "Give me some food, or you get the foo!"
},
{
img: "https://images.unsplash.com/photo-1584553391899-7b5b3287c66d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80",
alias: "vicky",
name: "Victoria Nul",
bio: "knock, knock - lets take the rock!"
},
{
img: "https://images.unsplash.com/photo-1636725360313-d37f4a232cfa?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2679&q=80",
alias: "Mixxy",
name: "Luna Lu",
bio: "I am just a beauty 🥰"
}
];
}
Here is -> hamsters marked <-
<main>
<div class="hamster-card_container">
<app-hamster-card *ngFor="let hamster of hamsters"
img={{hamster.img}}
alias={{hamster.alias}}
name={{hamster.name}}
bio={{hamster.bio}}>
</app-hamster-card>
</div>
<app-proposals
title={{title}}
hamsters={{hamsters}}>
</app-proposals>
</main>
CHILD
import { IHamster } from '../ihamster';
#Component({
selector: 'app-proposals',
templateUrl: './proposals.component.html',
styleUrls: ['./proposals.component.scss']
})
export class ProposalsComponent implements OnInit {
#Input() hamsters: IHamster[] = [];
#Input() title: string = "";
number: number = Math.floor(Math.random() * this.hamsters.length) + 1;
constructor() {
}
ngOnInit(): void {
}
}
The title is passed successfully.
<div class="proposals-container">
<p>{{title}}</p>
<!-- <ng-component *ngFor="let hamster of hamsters; let i = index">
<ng-component *ngIf="(i +1) % number == 0">
<app-proposals-profile
>
</app-proposals-profile>
</ng-component>
</ng-component> -->
</div>
I got the Error:
Error: src/app/app.component.html:15:5 - error TS2322: Type 'string' is not assignable to type 'IHamster[]'.
15 hamsters={{hamsters}}>
~~~~~~~~
src/app/app.component.ts:6:16
6 templateUrl: './app.component.html',
~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
✖ Failed to compile.
This might help. If you want to pass argument, you need to use [] to mark that value that you passing though is variable and not a string.
<app-proposals title={{title}} [hamsters]="hamsters"></app-proposals>

How to get Selected item value from a selectBox data

I have a json file contains objects that have sub-objects like that :
[
{
"index": 0,
"name": "Médecine et spécialités médicales",
"specialties": [
{
"id": 0,
"name": "Médecine interne"
},
{
"id": 1,
"name": "Maladies infectieuses"
},
{
"id": 2,
"name": "Carcinologie médicale"
}
]
},
{
"index": 1,
"name": "Chirurgie et spécialités chirurgicales",
"specialties": [
{
"id": 0,
"name": "Chirurgie générale"
},
{
"id": 1,
"name": "Chirurgie carcinologique"
},
{
"id": 2,
"name": "Chirurgie thoracique"
}
]
}
]
I want to get the value of selected item every time I change the selected data
here is stackblitz that I'm working on, the value of selected item shown as number, how can I get the value as string name field?
According to your stackblitz you passing in value the index not the category name ,
I would do something like this:
app.html:
<ng-template [ngIf]="selectedCategory">
<label for="category">Category</label>
<select
name="category"
id="category"
class="form-control"
(change)="onCategorySelected($event.target.value)"
>
>
<option
*ngFor="let category of categories; index as i;"
[value]="category.name"
>
{{ category.name }}
</option>
</select>
<label for="specialty">Specialties</label>
<select
[(ngModel)]="selectedSpecialty"
name="specialty"
id="specialty"
class="form-control"
(change)="onSpecialtySelected($event.target.value)"
>
<option
*ngFor="let specialty of selectedCategory.specialties;"
[value]="specialty.name"
>
{{ specialty.name }}
</option>
</select>
</ng-template>
I had an ng template to avoid console errors and change the value of the first select tag by category.name and remove ngModel (don't know why this doesn't work sorry).
app.ts:
import { Component, OnInit } from "#angular/core";
import { HttpClient, HttpClientModule } from "#angular/common/http";
interface Speciality{
name: string
}
interface Category{
name: string,
specialities: Speciality[]
}
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
categories: Category[];
selectedCategory: Category;
selectedSpecialty: Speciality;
constructor(private http: HttpClient) {
this.http.get<Category[]>("assets/json/data.json").subscribe(res => {
this.categories = res;
this.selectedCategory = this.categories[0]
});
}
onCategorySelected(categoryName: string) {
console.log(categoryName);
this.selectedCategory = this.categories.find(cat => cat.name === categoryName)
}
onSpecialtySelected(value: string) {
console.log(value);
this.selectedSpecialty.name = value;
}
}
I create interface to type your category and specialities, and in the subscribe method add a line to init your selectedCategory with the first object of your categories array.
here is the link to the stackblitz
you can use a getter. that's you create a function
categories: any=[]; //<--initizalize your variable with a empty array
get categoryName()
{
const cat=this.categories.find((_,index)=>index==this.selectedCategory)
return cat?cat.name:null
}
Then each time you write in .html
{{categoryName}} //you see the category selected
And you can forget all yours (change) in select. BTW, remove also the [selected]="i" in your .html. You are using [(ngModel)], so you needn't selected
your forked stackblitz

how to do ngFor inside ngFor dynamically in angular 8?

Hi what i trying to achieve is ngFor with dynamic value inside ngFor, is this possible? i try using ngModel inside it too and it didn't work out. Here is what i do :
inside my home.component.ts :
import { Component, OnInit } from '#angular/core';
import {CdkDragDrop, moveItemInArray} from '#angular/cdk/drag-drop';
export interface Condition {
value: string;
viewValue: string;
}
export interface ListProduk {
value: string;
viewValue: string;
}
export interface DragBox {
value: string;
viewValue: string;
}
export interface ListModel {
value: string;
viewValue: string;
single_item: string;
}
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
conditions: Condition[] = [
{ value: 'if', viewValue: 'IF' },
{ value: 'else', viewValue: 'ELSE' },
{ value: 'then', viewValue: 'THEN' },
{ value: 'if else', viewValue: 'IF ELSE' },
{ value: 'or', viewValue: 'OR' },
{ value: 'and', viewValue: 'AND' }
];
listProduks: ListProduk[] = [
{ value: 'mcm-508', viewValue: 'MCM-508' },
{ value: 'bl-100 pl', viewValue: 'BL-100 PL' },
{ value: 'bl-150 bl', viewValue: 'BL-150 BR' },
{ value: 'bl-302gs', viewValue: 'BL-302GS' },
{ value: 'bl-52gl', viewValue: 'BL-52GL' }
];
listModels: ListModel[] = [
{ value: 'conditions', viewValue: 'Condition', single_item:'condition' },
{ value: 'listProduks', viewValue: 'List Produk', single_item:'listProduk' },
]
constructor() { }
ngOnInit() {
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.listModels, event.previousIndex, event.currentIndex);
}
}
and then here is my home.component.html :
<p>home works!</p>
<div cdkDropList cdkDropListOrientation="horizontal" class="example-list" (cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let listModel of listModels" cdkDrag>
<mat-form-field>
<mat-label>Pick {{listModel.value}} :</mat-label>
<mat-select>
<mat-option *ngFor="let {{listModel.single_item}} of {{listModel.value}}" [value]="{{listModel.single_item}}.value">
test
</mat-option>
</mat-select>
</mat-form-field>
<div>
<i class="material-icons">
arrow_right_alt
</i>
</div>
</div>
</div>
i try to do loop the mat-select dynamically, since i want it loop an array that have different name, i need value in listModel array to print to *ngFor inside mat-select. Which is this line :
<mat-option *ngFor="let {{listModel.single_item}} of {{listModel.value}}" [value]="{{listModel.single_item}}.value">
test
</mat-option>
how to do this properly?
UPDATED QUESTION After update my code with Ahmed comment, which is my Html is looked like this :
<p>home works!</p>
<div cdkDropList cdkDropListOrientation="horizontal" class="example-list" (cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let listModel of listModels" cdkDrag>
<mat-form-field>
<mat-label>Pick {{listModel.value}} :</mat-label>
<mat-select>
<mat-option *ngFor="let a of listModel.value" [value]="a.value">
{{a.viewValue}}
</mat-option>
</mat-select>
</mat-form-field>
<div>
<i class="material-icons">
arrow_right_alt
</i>
</div>
</div>
</div>
and this give me an error like this :
ERROR Error: Cannot find a differ supporting object 'conditions' of
type 'string'. NgFor only supports binding to Iterables such as
Arrays.
what did i missed?
You can display this using a function, which would return the correct array. We are calling this in the template. BE CAREFUL, I would never recommend calling a function in template, if it is just all possible. This can seriously hurt performance in an app. But if you don't have much content on this page, it is pretty safe to use. So I would suggest the following:
<div *ngFor="let value of getList(listModel.value)">
and the function would return the correct array:
getList(value) {
return this[value]
}
You could also make a slight change to the model and pass an optional parameter with the array with the correct array to the object itself. You can do this in OnInit:
ngOnInit() {
this.listModels.forEach(x => {
x.customArray = this[x.value]
})
}
and use it like normal iteration in *ngFor:
<div *ngFor="let value of listModel.customArray">
Here's a STACKBLITZ with both options

Filter array of objects in Angular4 without pipe

I have an array of links that each element is an object that contain few strings - a link, description and category. I have different components that display the links, and I want in each component to display only the links of its category.
So I want to filter the array by the category.
I have a mock-up array with all the links.
I try to filter the array of objects without a pipe. The reason why: https://angular.io/guide/pipes#appendix-no-filterpipe-or-orderbypipe
Apparently the Angular team suggests to do the filtering in the component level and not using a pipe:
"The Angular team and many experienced Angular developers strongly recommend moving filtering and sorting logic into the component itself."
So here's my component:
#Component({
selector: 'startups',
templateUrl: './startups.component.html'
})
export class StartupsComponent implements OnInit {
constructor(private appLinksService: DigitalCoinHubService) { }
title = 'Startups';
links: DCHlinks[]; // create a new array in type of DCHlinks to get the data
startupsLinks: DCHlinks [] = []; // will build the startsups links only
getLinks(): void {
this.links = this.appLinksService.getLinks(); // gets the array with the data from the service
for (let i in this.links)
{
if (this.links[i].dchCategory == 'startups' )
{
this.startupsLinks[i].push(this.links[i]);
}
}
}
ngOnInit() {
this.getLinks();
}
}
So first I get the big array from the service:
this.links = this.appLinksService.getLinks();
And then I try to build a new array that will contain only the relevant links. The filter is by the category. But then when I try to build the new array by push the elements which their category matches - it gives me error:
Property 'push' does not exist on type 'DCHlinks'.
DCHlinks is the object - this is the class:
export class DCHlinks {
dchLink: string;
dchLinkTitle: string;
dchLinkDescription: string;
dchCategory: string;
}
Any idea how to do this simple filter? (and w/o pipe - see above reason why..)
Thanks!
You need to intialize the array as you did for startupsLinks
links: DCHlinks[] = [];
Or you can simply use array.filter to get the relavant data
this.startupsLinks = this.links.filter(t=>t.dchCategory == 'startups');
I have the same issue but I resolve in different way. I have the Array of object
and want to use filtering using of html select option .which I given the data that filter the array of object.
`$`
heroes = [{
name: "shubh",
franchise: "eng"
},
{
name: "Ironman",
franchise: "Marvel"
},
{
name: "Batman",
franchise: "DC"
},
{
name: "Batman",
franchise: "DC"
},
{
name: "Batman",
franchise: "DC"
},
{
name: "satman",
franchise: "mc"
},
{
name: "monmam",
franchise: "DC"
},
{
name: "loolman",
franchise: "DC"
},
{
name: "Thor",
franchise: "Marvel"
},
{
name: "monmam",
franchise: "DC"
},
{
name: "monmam",
franchise: "DC"
},
{
name: "monmam",
franchise: "DC"
},
{
name: "Thor",
franchise: "Marvel"
},
{
name: "Superman",
franchise: "DC"
},
{
name: "Superman",
franchise: "DC"
},
{
name: "Superman",
franchise: "DC"
},
{
name: "Superman",
franchise: "DC"
},
];
//this is the most imp part in the filter section .I face lot of problem this is not working if this line not write .The filter method works old one time.
newarr = this.heroes;
//So I create new array which store the old array value every time. and we replace the value in the filter function.
filter(heroes: string) {
console.log(heroes);
this.heroes = this.newarr; // replace the value and store old value
let heroesnew = this.heroes.filter((data => data.name == heroes));
this.heroes = heroesnew;
console.log(this.heroes);
}
<!––"#sel" is the template variable which gives the data of value property in option field ––>
<select #sel class="custom-select custom-select-sm" (change)="filter(sel.value)">
<option>All</option>
<option value="Batman">Batman</option>
<option value="Superman">Superman</option>
<option value="satman">satman</option>
<option value="monmam">monmam</option>
<option value="Thor">thor</option>
</select>
<!–– this is the table that I want to filter––>
<div>
<table class="table table-hover ">
<thead class="thead-dark">
<tr>
<th>#</th>
<th>name</th>
<th>franchise</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let incidence of heroes">
<td>{{ incidence.name}}</td>
<td> {{ incidence.franchise }}</td>
</tr>
</tbody>
</table>
</div>
$
use the code for angular 2+,to 8 It working fine ..

Resources