2

I've been going back and forth this whole weekend of how I'm supposed to think when structuring models and classes for my project and I just can't wrap my brain around it.

I'm gonna try and explain as best as I can (and please comment if I should explain further), I have a remote api where I pick up some lists that are going to be displayed in an app. The response from the API is in JSON and I get my lists in a simple array structure containing objects of my list.

Here's a small excerpt of the RemoteService provider where i fetch the lists:

export class RemoteService<Type> {
  public resource: string;
  private actionUrl: string;
  private headers: Headers;

  constructor(private _http: Http) {
      this.actionUrl = 'API_URL';

      this.headers = new Headers();
      this.headers.append('Content-Type', 'application/json');
      this.headers.append('Accept', 'application/json');
  }

  public GetAll = (): Observable<Type[]> => {
    return this._http.get(this.actionUrl + this.resource)
      .map((response: Response) => <Type[]>response.json())
      .catch(this.handleError);
  }
}

When I decide to fetch all the lists on my Home page I then load them into an Observable Array in the page's controller where I then map the result and instantiate every single object into a ListModel.

export class HomePage {

  lists: Observable<Array<ListModel>>;
  listsObserver: any;

  constructor(public nav: NavController, public remoteService: RemoteService<ListModel>) {
    this.lists = Observable.create(observer => {
      this.listsObserver = observer;
    });

    this.remoteService.resource = 'lists';

    this.remoteService
      .GetAll()
      .subscribe((data:ListModel[]) => {
          this.listsObserver.next(data.map(list => new ListModel(list.lid, list.title, list.items)));
        },
        error => console.log(error),
        () => console.log('Get all Items complete'));
  }
}

And a ListModel looks like the following:

export class ListModel {

  constructor(public lid: number, public title: string, public items: any[]) {
    this.lid = lid;
    this.items = items;
  }
}

After doing all of this I started doubting myself if this is a good way to go about it. I want to understand how to really make correct use of angular 2..

So finally to my questions:

First of all, should I create the observer in the ListModel instead of every page where I want to display or fetch my lists? Or should I create a separate ListCollection for this purpose and then load it where I need it? If not the latter, then how should I think?

Then another bonus question, is it possible to cast the loaded object from the RemoteService into a dynamic Model depending on where I load it? I read the top comment here and came to the conclusion that there should be a way to do this right?

Best regards,

Jake the Snake (not the wrestler)

jzasnake
  • 70
  • 9
  • Are you comfortable with the generic `RemoteService`? I only ask because it's the first thing that stands out to me as an anti-pattern e.g. what if one of your types doesn't support the `GetAll()` method or other methods that are added to it later? – Brad Sep 22 '16 at 00:04
  • Why would this be an issue? Even if one of my Models didn't support the Update method, I just wouldn't use it. Or am I understanding you wrong? – jzasnake Sep 22 '16 at 18:45

1 Answers1

1

From the code you have posted you generally seem to be on the right track! Lets look at some details.

Create real class instances in RemoteService

Currently, the RemoteService casts to the type, but that does not instantiate a real object of the type. If you want to achieve a generic solution, you should use the factory pattern (details omitted for brevity):

export interface TypeFactory<Type> {
    public create(data:any):Type;
}
export class RemoteService<Type> {
  constructor(private _http: Http, private factory:TypeFactory<Type>) {
      // ...
  }

  public GetAll(): Observable<Type[]> => {
    return this._http.get(this.actionUrl + this.resource)
        .map((response: Response) => response.json())
        .map((data:any) => {
            // Now `data` should be an array of the expected type, depending on the server side.
            // So we need to pass each element to the factory to create an actual instance.
            // We will then return the array of instances to be emitted to the observable with the correct type.
            return data.items.map((item:any) => this.factory.create(data));
        })
        .catch(this.handleError);
  }
}

Now the GetAll() observable really emits Type instances, given the factory does its job.

Defining a TypeFactory

The job of the TypeFactory is to turn an arbitrary object into a concrete class instance. So lets see how that would look like for the ListModel:

export class ListModelFactory implements TypeFactory<ListModel> {
    public create(data:any):ListModel {
        // todo: validate data
        return new ListModel(data.lid, data.title, data.items);
    }
}

Connecting the parts in the component

Now you can connect both parts to achieve what you want (i just assumed your HomePage is actually a @Component):

@Component(...)
export class HomePage implements OnInit {

    lists: Array<ListModel>;

    constructor(public nav: NavController, public remoteService: RemoteService<ListModel>) {
    }

    ngOnInit() {
        this.remoteService
            .GetAll()
            .subscribe(
                (data:ListModel[]) => this.lists = data,
                error => console.log(error),
                () => console.log('Get all Items complete')
            );
    }
}

In your view you can now access the lists property to output your data once the observable has resolved a value. With the AsyncPipe you can further simplify your component.

The actual factory is hidden in the RemoteService<ListModel> and needs to be injected using DI.


Update: Implementing the AsyncPipe. The HomeComponent now boils down to:

@Component({
    template: `<div *ngFor="let listModel of lists | async"> ... {{listModel}} ...</div>`
})
export class HomePage implements OnInit {

    lists: Observable<Array<ListModel>>;

    constructor(public nav: NavController, public remoteService: RemoteService<ListModel>) {
    }

    ngOnInit() {
        this.lists = this.remoteService.GetAll();
    }
}
Fabian Kleiser
  • 2,988
  • 3
  • 27
  • 44
  • Thank you! I had completely forgotten about the use of factories.. So in your example of the HomePage component (and yes you assumed correctly) the remoteService loads all the data into the component. Is that the "correct" way of doing this or can I put all the methods for fetching/posting data in the `ListModelFactory` instead? Not only would the code look cleaner, but it does feel more correct to me. Eg. I would call `this.lists = ListModel.GetAll();` and that would return a ListModel Observable Array in my HomePage and my ListsPage component. I don't even know if I'm overthinking this.. – jzasnake Sep 22 '16 at 18:41
  • A `Factory` class should always only be concerned with creating objects of a single type (in your case the `ListModel`). So don't do any fetches/posts in there. I updated the answer to declutter the `HomeComponent` with the `AsyncPipe`. Also note the corresponding template syntax which I have inlined in the `@Component` decorator. – Fabian Kleiser Sep 22 '16 at 19:50
  • Im not able to get the `TypeFactory` running.. I am sure Im doing something wrong with the DI and I've tried looking for other examples.. Could you show me an example of how I inject it properly? – jzasnake Sep 24 '16 at 16:31
  • It's a bit difficult to see where you are struggling, it might be better if you ask another question. However, there is a good blog post on [Angular2 DI](http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular-2.html) by thoughtram, which might shed some light. If you are trying to inject interfaces, you should know how the [OpaqueToken](http://stackoverflow.com/a/38702867/1262901) works. – Fabian Kleiser Sep 25 '16 at 21:30