0

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:

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.

Vardane
  • 13
  • 4

1 Answers1

1

Finally, after some trial and error, I solved the issue as follows:

  1. I edited the Fragment model in fragment.ts by making Verse an array. The line
public verses?: Verse;

has been replaced with

public verses?: Verse[];
  1. I also edited the code base in the back-end (which uses Django Rest Framework): I added the field "fragments" to the Chapter serializer, as well as an explicit reference to the Fragment serializer. As a result, I also had to move the Fragment serializer above the Chapter serializer in the serializer.py file, since now Chapter serializer was referring to the FragmentSerializer class. That way, requesting the url associated with the ChapterList view (inheriting from generics.ListAPIView) returned a nested json file including not only every fragment under every chapter, but also every verse under every fragment, so a double level of nesting, which wasn't the case before.
class FragmentSerializer(serializers.ModelSerializer):
    """
    Fragment in a chapter
    Returns chapter and book objects upward
    as well as all verses downward (reverse relationship) 
    """

    class Meta:
        model = Fragment
        depth = 2
        fields = [
            'id',
            'chapter_id',
            'surtitle',
            'title_string',
            'title_format',
            'verses',
        ]


class ChapterSerializer(serializers.ModelSerializer):
    """
    Chapters
    """
    # https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects
    fragments = FragmentSerializer(many=True)
    class Meta:
        model = Chapter
        depth = 1 # allows access to the whole book object
        fields = ['id', 'number', 'book_id', 'fragments',]

  1. Now, in the livre-detail.component.html file, every verse property could be accessed using dot notation:
<div *ngFor= 'let chapter of chapters' >
  <span class="chapter_number">
    Chapitre {{ chapter.number }}
  </span>
  <p></p>
  <div *ngFor= 'let fragment of chapter.fragments'>
    <p>{{ fragment.title_string }}</p>
        <div *ngFor= 'let verse of fragment.verses'>
            {{ verse.number }}
            {{ verse.text }}     
        </div>
        <p></p>          
  </div>
</div>

  1. The final clean typescript file livre-detail.component.ts being:
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, from} from 'rxjs';
import {map} from 'rxjs/operators';
import { Livre } from '../models/livre';
import { Chapter } from '../models/chapter';
import { Fragment } from '../models/fragment';
import {ResponseApi} from '../models/api';

import { LivreService } from '../services/livre.service';
import { ChapterService} from '../services/chapter.service';


@Component({
  selector: 'app-livre-detail',
  templateUrl: './livre-detail.component.html',
  styleUrls: ['./livre-detail.component.scss']
})
export class LivreDetailComponent implements OnInit {

  livres$!: Observable<Livre[]>;
  livres: Livre[] | undefined;
  livre: Livre | undefined;
  livre_name_ordinal: number | undefined;
  livre_name_text: string | undefined;
  
  chapters$!: Observable<Chapter[]>;
  chapters: Chapter[] | undefined;
  chapter: Chapter | undefined;
  chapter_number_as_string: string | undefined;
  
  fragments$!: Observable<Fragment[]>;
  fragment: Fragment | undefined;
  fragments: Fragment[] | undefined;
  fragment_id: number | undefined;
  fragment_id_as_string: string | undefined;
  title_string: string | undefined;

  constructor(
    private route: ActivatedRoute,
    private livreService: LivreService,
    private chapterService: ChapterService,
  ) { }

  ngOnInit(): void {
        // First get the book diminutive from the current route.
        const routeParams = this.route.snapshot.paramMap;
        const bookDiminutiveFromRoute = String(routeParams.get('bookDiminutive'));
        console.log(bookDiminutiveFromRoute)

        // Get the name of the book whose detail is displayed on this page
        this.livres$ = this.livreService.filterList(
          'diminutive', bookDiminutiveFromRoute).pipe(
            map((responseApi: ResponseApi<Livre>) => {
              return responseApi.results;
            }
          )
        );
        this.livres$.subscribe((livres: Livre[]) => {
          this.livres = livres;
          // Actually a single book gets returned, so:
          this.livre = this.livres[0]
          this.livre_name_ordinal = this.livre.name_ordinal
          this.livre_name_text = this.livre.name_text
        });

        // List chapters that belong to the book being displayed
        this.chapters$ = this.chapterService.filterList(
          'book_id__diminutive', bookDiminutiveFromRoute).pipe(
            map((responseApi: ResponseApi<Livre>) => {
              return responseApi.results;
            }
          )
        );
        this.chapters$.subscribe((chapters: Chapter[]) => {
          this.chapters = chapters
        });
      }
    }

Happy to hear about any way to improve on that solution.

Vardane
  • 13
  • 4