0

I'm trying to make a service that gets a post from a json file containing array of posts. i have already a service that returns a json file contents by using HttpClient. the goal is to show the full post contents.

the service that gets a json file:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class GetJsonFileService {

  constructor(private http: HttpClient) {}

  getJsonFile(jsonFile: string /* the file path is set by components */){
    return this.http.get(jsonFile,{observe: 'body', responseType: 'json'});
  }
}

the service that gets a post by its id:

import { Injectable } from '@angular/core';
import { GetJsonFileService } from './get-json-file.service';

@Injectable({
  providedIn: 'root'
})
export class GetPostService {

  constructor(private getJsonFileservice: GetJsonFileService) {}

  getPost(id: number) :object{
    var post!: object;
    this.getJsonFileservice.getJsonFile('assets/posts.json').subscribe((data :any)=>{
      post = data["posts"][id];
    });
    return post;
  }

}

the component that shows the post:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { GetPostService } from '../services/get-post.service';

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

  _post!: object;
  id!: number;

  constructor(private route: ActivatedRoute, private getPost: GetPostService){}

  ngOnInit(){
    var id!: number;
    this.route.params.subscribe(params=>{
      id = params.id;
    });
    this._post = this.getPost.getPost(id);
  }
}

when i try to show something in the component template like: {{_post.title}}

i get this error: Errors while compiling. Reload prevented.

and in vscode typescript tells me this: Property 'title' does not exist on type 'object'

I Tried @msanford's solution. there still one thing to change in post-view but i don't really know how to do it: posts["posts"][id] typescript gives me an error:

Element implicitly has an 'any' type because expression of type '"posts"' can't be used to index type 'Object'.
  Property 'posts' does not exist on type 'Object'

any ideas?

muaaz
  • 101
  • 11
  • `posts["posts"][id]` gives you an accessor error: what does the data structure actually look like, can you share it? I simply repeated `data["posts"][id];` from your question. – msanford Oct 06 '21 at 14:30
  • (Note that there were several other things to change as well.) – msanford Oct 06 '21 at 14:40
  • @msanford , Yes I found several problems by checking the docs of observable [link](https://angular.io/guide/observables) it seems i have to change many things. but your answer helped me to understand observables. thanks – muaaz Oct 06 '21 at 15:31

3 Answers3

6

You have declared your post to be of type object: _post!: object; and, well, Property 'title' does not exist on type 'object'.

You should declare an interface or class with the fields of what a post actually contains. Just as an example:

interface Post {
  id: number;
  title: string;
  body: string;
  author: string;
  // etc
}

And use that in your type declaration: _post: Post;

That said, as others have pointed out, your service and component logic needs work.

You don't typically .subscribe() in a service, but in the consumer of the service, which is the component. So, instead of

// Service
getPost(id: number) :object{
    var post!: object;
    this.getJsonFileservice.getJsonFile('assets/posts.json').subscribe((data :any)=>{
      post = data["posts"][id];
    });
    return post;
  }

Use simply

// Service
getPost = (): Post  => this.getJsonFileservice.getJsonFile<Post[]>('assets/posts.json')

(Edit: I've cast your service return above in the customary way by adding <Post[]> to the call, but since I have not yet seen your data structure, it may require something different.)

And then Subscribe in your Component

ngOnInit(){
    let id: number;

    this.route.params.subscribe(
        params => id = params.id,
        error => console.error,
        complete => {
          this.getPost.getPost().subscribe(
            posts => this._post = posts["posts"][id],
            error => console.error
          );
        }
    );    
  }

where you also had a race condition where you could have asked for a post before the router sent you params with the id, which is presumably why you added the non-null-assertion operator id !: number because the compiler told you it might be null. There was a reason for that, don't override it.

msanford
  • 11,803
  • 11
  • 66
  • 93
4

You have a problem that you are returning the post before it is assigned:

post = data["posts"][id];

The best way is to return an observable and subscribe to it whenever you want the value provided.

Make sure to read the Documentation carefully to understand how observables work.

https://angular.io/guide/observables

msleiman
  • 146
  • 5
0

Since Angular is using Typescript, you'll have to either mark _post as any or create a typescript interface/type for it with the fields you're expecting to have.

Oh you can also do this:

export class PostViewComponent implements OnInit {

  _post!: {
      title: string
  };
  id!: number;

  constructor(private route: ActivatedRoute, private getPost: GetPostService){}

  ngOnInit(){
    var id!: number;
    this.route.params.subscribe(params=>{
      id = params.id;
    });
    this._post = this.getPost.getPost(id);
  }
}
Yom B
  • 228
  • 1
  • 10
  • But `route.params.subscribe()` is asynchronous, so you cannot guarantee that you will have `id` set by the time you make the `getPost()` call. For that, you need to `getPost()` inside the `oncomplete` handler to `params.subscribe()` (or, less commonly, in `onnext`). – msanford Oct 06 '21 at 14:39