The model is as follows: A book comprises several fragments which each comprise chapters which in turn comprise verses.
For fragments, the property of interest is the title. For verses, the properties of interest are verse number and verse text. (Chapters data is of no interest to the user).
Here is the code for the relevant models:
Fragments.ts:
import {Deserializable} from './deserializable';
import { Chapter } from './chapter';
import { Verse } from './verse';
export class Fragment implements Deserializable {
public id?: number;
public url?: string;
public surtitle?: string;
public title_string?: string;
public title_format?: number;
public verses?: Verse;
public chapter_id?: Chapter;
deserialize(input: any): this {
Object.assign(this, input);
return this;
}
chapters.ts:
import {Deserializable} from './deserializable';
import { Livre } from './livre';
export class Chapter implements Deserializable {
public id?: number;
public url?: string;
public number?: number;
public book_id?: Livre;
deserialize(input: any): this {
Object.assign(this, input);
return this;
}
}
verse.ts:
import {Deserializable} from './deserializable';
import { Fragment } from './fragment';
export class Verse implements Deserializable {
public id?: number;
public url?: string;
public number?: number;
public text?: string;
public optional_indication?: number;
public fragment_id?: Fragment;
deserialize(input: any): this {
Object.assign(this, input);
return this;
}
}
The goal is to display the content of a book to the user in a web page: that is the title of a fragment, then its verses, then the title of the next fragment, then its verses etc.
Currently, code in the relevant component, named "livre-detail.component.ts", gets the whole content of a book, including fragments and nested data, down to the text of every verse as "this fragment" and the JSON data is properly logged in the console, or in the browser when the template simply goes:
<div *ngFor= 'let fragment of fragments'>
{{ fragment | json}}
</div>
In the template, when code loops through the fragments using *ngFor directive, the title of each fragment is properly displayed ("fragment.title_string").
But I could not come up with a nested loop, resulting in displaying the text of each verse within each fragment.
I've tried multiple things:
using Angular property keyvalue as suggested in Angular2 - *ngFor / loop through json object with array
creating a variable for the verses in the component file using a nested map as suggested in Angular2 nested *ngFor with the code at Angular2 nested *ngFor (see code under Alternative 2 below) In my case, fragments are like hueGroups and verses are like lights.
Here is my current code:
livre-detail-component.ts:
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, from} from 'rxjs';
import {map} from 'rxjs/operators';
import { Fragment } from '../models/fragment';
import { Verse } from '../models/verse';
import {ResponseApi} from '../models/api';
import { FragmentService } from '../services/fragment.service';
@Component({
selector: 'app-livre-detail',
templateUrl: './livre-detail.component.html',
styleUrls: ['./livre-detail.component.scss']
})
export class LivreDetailComponent implements OnInit {
fragments$!: Observable<Fragment[]>;
fragment: Fragment | undefined;
fragments: Fragment[] | undefined;
verse: Verse | undefined;
//verses: Verse[] | undefined;
text: String | undefined;
// verseId: number | undefined;
constructor(
private route: ActivatedRoute,
private fragmentService: FragmentService,
) { }
ngOnInit() {
// First get the book diminutive from the current route.
const routeParams = this.route.snapshot.paramMap;
const bookDiminutiveFromRoute = String(routeParams.get('bookDiminutive'));
// Find the fragments that belong to the book with the diminutive provided in route.
// Note: a fragment belongs to a chapter which in turn belongs to a route.
this.fragments$ = this.fragmentService.filterList(
'chapter_id__book_id__diminutive', bookDiminutiveFromRoute).pipe(
map((responseApi: ResponseApi<Fragment>) => {
console.log(responseApi.results)
return responseApi.results;
})
);
this.fragments$.subscribe((fragments: Fragment[]) => {
this.fragments = fragments;
console.log(this.fragments)
});
}
livre-detail-component.html:
<div *ngFor= 'let fragment of fragments'>
<h3>{{ fragment.title_string }}</h3>
{{fragment.verses}}
</div>
The above returns "[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object] " under each fragment title:
Alternative 1: template with a nested loop as shown below:
<div *ngFor= 'let fragment of fragments'>
<h3>{{ fragment.title_string }}</h3>
<div>
<div *ngFor= 'let verse of fragment.verses'>
{{ verse.text }}
</div>
</div>
This returns the following error message:
"Type 'Verse | undefined' is not assignable to type 'any[] | Iterable | (Iterable & any[]) | (any[] & Iterable) | null | undefined'."
Alternative 2: attempt at nested map in the component file:
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, from} from 'rxjs';
import {map} from 'rxjs/operators';
import { Fragment } from '../models/fragment';
import { Verse } from '../models/verse';
import {ResponseApi} from '../models/api';
import { FragmentService } from '../services/fragment.service';
@Component({
selector: 'app-livre-detail',
templateUrl: './livre-detail.component.html',
styleUrls: ['./livre-detail.component.scss']
})
export class LivreDetailComponent implements OnInit {
fragments$!: Observable<Fragment[]>;
fragment: Fragment | undefined;
fragments: Fragment[] | undefined;
verse: Verse | undefined;
verses: Verse[] | undefined;
text: String | undefined;
verseId: number | undefined;
constructor(
private route: ActivatedRoute,
private fragmentService: FragmentService,
) { }
ngOnInit() {
// First get the book diminutive from the current route.
const routeParams = this.route.snapshot.paramMap;
const bookDiminutiveFromRoute = String(routeParams.get('bookDiminutive'));
// Find the fragments that belong to the book with the diminutive provided in route.
// Note: a fragment belongs to a chapter which in turn belongs to a route.
this.fragments$ = this.fragmentService.filterList(
'chapter_id__book_id__diminutive', bookDiminutiveFromRoute).pipe(
map((responseApi: ResponseApi<Fragment>) => {
console.log(responseApi.results)
return responseApi.results;
})
);
this.fragments = fragments.map((fragment: Fragment)=>{
let verseObjects = this.verses.map((verseId: number) =>{
return this.verses?.find((verse, index) => {return index === verseId})
});
fragment.verses = verseObjects;
return fragment.verses
});
}
This triggers the following error message:
Error: src/app/livre-detail/livre-detail.component.ts:54:11 - error TS2741: Property 'deserialize' is missing in type '(Verse | undefined)[]' but required in type 'Verse'.
54 fragment.verses = verseObjects; ~~~~~~~~~~~~~~~
src/app/models/verse.ts:13:3 13 deserialize(input: any): this { ~~~~~~~~~~~ 'deserialize' is declared here. "
For reference, the verse.ts includes the model for Verse and is as follows:
import {Deserializable} from './deserializable';
import { Fragment } from './fragment';
export class Verse implements Deserializable {
public id?: number;
public url?: string;
public number?: number;
public text?: string;
public optional_indication?: number;
public fragment_id?: Fragment;
deserialize(input: any): this {
Object.assign(this, input);
return this;
}
}
Also for reference, deserializable.ts is as follows:
export interface Deserializable {
deserialize(input: any): this;
}
Any help would be greatly appreciated.