4

I've been working on this for a while now and can't seem to find an answer clear enough to understand. I have a TestComponent that grabs an array of TestModels from a server using TestService. When I grab these test models it is just a json file that the server is reading and sending back with the correct mime type. Once the test models are grabbed from the server, I put them in a simple select element drop down. When a test model is selected it display the selected test model in a nested component, TestDetailComponent.

That is all well and good and is working fine. I keep running into issues when I am pulling in the data from the server. Since JavaScript has no runtime checking we can't automatically cast the JSON from the server to a typescript class so I need to manually create a new instance of the TestModel with the retreived JSON.

Okay so here is the problem. I need to call new TestModel and give it its dependencies but it needs to be a new instance of TestModel. I want the TestModel to be able to save and update itself back to the server so it has a dependency on Http from @angular/core and it has a dependency on a config class I made that the angular injects with an opaqueToken, CONFIG.I can't figure out how to get new instances of TestModel. Here are the initial files

TestComponent:

import { Component, OnInit } from '@angular/core';

import { TestService } from './shared/test.service';
import { TestModel } from './shared/test.model';
import { TestDetailComponent } from './test-detail.component';

@Component({
    selector: "test-component",
    templateUrl: 'app/test/test.component.html',
    styleUrls: [],
    providers: [TestService],
    directives: [TestDetailComponent]
})
export class TestComponent implements OnInit {

    tests: TestModel[] = [];
    selectedTest: TestModel;

    constructor(private testService: TestService) {};

    ngOnInit() {
        this.testService.getTestsModels().subscribe( (tests) => {
            console.log(tests);
            this.tests = tests 
        });
    }
}

TestComponent template:

<select [(ngModel)]="selectedTest">
    <option *ngFor="let test of tests" [ngValue]="test">{{test.testing}}</option>
</select>
<test-detail *ngIf="selectedTest" [test]="selectedTest"></test-detail>

TestDetailComponent:

import { Component, Input } from '@angular/core';
import { JsonPipe } from '@angular/common';

import { TestModel } from './shared/test.model';

@Component({
    selector: 'test-detail',
    templateUrl: 'app/test/test-detail.component.html',
    pipes: [JsonPipe]
})
export class TestDetailComponent {
    @Input() test;
}

TestDetailComponent template

<p style="font-size: 3em;">{{test | json}}</p>

TestModel

import { Injectable, Inject } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Rx';

import { CONFIG } from './../../config/constants';

@Injectable()
export class TestModel {

    "testing": number;
    "that": string;
    "a": string;

    constructor(private http: Http, @Inject(CONFIG) private config) {}

    save(): Observable<TestModel[]> {

        let url = this.config.apiUrl + "test";
        let body = JSON.stringify({
            testing: this.testing,
            this: this.that,
            a: this.a
        });
        let headers = new Headers({'Content-Type': 'application/json'});
        let options = new RequestOptions({headers: headers});

        return this.http.post(url, body, options)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            results.map( (aggregate, current) => {
                                aggregate.push(<TestModel>current);
                                return aggregate;
                            }, new Array<TestModel>())
                        }).catch(this.handleError);

    }

    update() {

        let url = this.config.apiUrl + "test";
        let body = JSON.stringify({
            testing: this.testing,
            this: this.that,
            a: this.a
        });
        let headers = new Headers({'Content-Type': 'application/json'});
        let options = new RequestOptions({headers: headers});

        return this.http.put(url, body, options)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            results.map( (aggregate, current) => {
                                aggregate.push(<TestModel>current);
                                return aggregate;
                            }, new Array<TestModel>())
                        }).catch(this.handleError);

    }

    private handleError(err): Observable<any> {

        let errMessage = err.message ? err.message : err.status ? `${err.status} - ${err.statusText}` : 'Server Error';

        return Observable.throw(new Error(errMessage));

    }

}

Test Service

import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';

import { CONFIG } from './../../config/constants';
import { TestModel } from './test.model';

@Injectable()
export class TestService {

    constructor(private http: Http, @Inject(CONFIG) private config) {}

    getTestsModels(): Observable<TestModel[]> {

        let url = this.config.apiUrl + "test";

        return this.http.get(url)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            return results.map( (current) => {
                                return <TestModel>current; // <<<--- here is the error
                            })
                        })
                        .catch(this.handleError);

    }

    private handleError(err): Observable<any> {

        let errMessage = err.message ? err.message : err.status ? `${err.status} - ${err.statusText}` : 'Server Error';

        return Observable.throw(new Error(errMessage));

    }

}

I have tried using the ReflectiveInjector so TestService becomes this:

    import { Injectable, Inject, ReflectiveInjector } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';

import { CONFIG } from './../../config/constants';
import { TestModel } from './test.model';

@Injectable()
export class TestService {

    constructor(private http: Http, @Inject(CONFIG) private config) {}

    getTestsModels(): Observable<TestModel[]> {

        let url = this.config.apiUrl + "test";

        return this.http.get(url)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            return results.map( (current) => {
                                return ReflectiveInjector.resolveAndCreate([TestModel]).get(TestModel);
                            })
                        })
                        .catch(this.handleError);

    }

    private handleError(err): Observable<any> {

        let errMessage = err.message ? err.message : err.status ? `${err.status} - ${err.statusText}` : 'Server Error';

        return Observable.throw(new Error(errMessage));

    }

}

But then I just get the error:

enter image description here

Then if I add Http to the ReflectiveInjector I just get another connection backend error and I am assuming that would keep going to the dependency chain till we found the bottom.

Sorry for the long post, any help would be appreciated!

CSchulz
  • 10,882
  • 11
  • 60
  • 114
Beamer180
  • 1,501
  • 5
  • 19
  • 25

3 Answers3

11

You can provide a factory function. This is different from a simple useFactory: ... provider like

{ 
    provide: 'TestModelFactory', 
    useFactory: () => {
        return (http, config) => { 
            return new TestModel(http, config);
        };
    },
    deps: [Http, CONFIG];
}

and then use it like

@Injectable()
export class TestService {

   constructor(@Inject('TestModelFactory' testModelFactory) {}

   getTestsModels(): Observable<TestModel[]> {
        let url = this.config.apiUrl + "test";
        return this.http.get(url)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            return results.map( (current) => {
                                let tm = testModelFactory();
                                tm.xxx // assign data
                            })
                        })
                        .catch(this.handleError);
    }
}

You can also support per instance parameters like

{ 
    provide: 'TestModelFactory', 
    useFactory: (json) => {
        return (http, config) => { 
            return new TestModel(http, config, json);
        };
    },
    deps: [Http, CONFIG];
}

and then use it like

@Injectable()
export class TestService {

   constructor(@Inject('TestModelFactory' testModelFactory) {}

   getTestsModels(): Observable<TestModel[]> {
        let url = this.config.apiUrl + "test";
        return this.http.get(url)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            return results.map( (current) => {
                                let tm = testModelFactory(result);
                            })
                        })
                        .catch(this.handleError);
    }
}

But you don't need to use DI. You already inject Http and CONFIG into your TestService. You can just

@Injectable()
export class TestService {

    constructor(private http: Http, @Inject(CONFIG) private config) {}

    getTestsModels(): Observable<TestModel[]> {

        let url = this.config.apiUrl + "test";

        return this.http.get(url)
                        .map( (response) => response.json() )
                        .map( (results) => {
                            return results.map( (current) => {
                                return new TestModel(http, config);
                            })
                        })
                        .catch(this.handleError);

    }

    private handleError(err): Observable<any> {

        let errMessage = err.message ? err.message : err.status ? `${err.status} - ${err.statusText}` : 'Server Error';

        return Observable.throw(new Error(errMessage));

    }
}

In every case you need to provide some way to initialize TestModel from result for example by passing the JSON to the constructor and initialize the members of TestModel from the passed JSON.

See also Angular2: How to use multiple instances of same Service?

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Thank you for you answer. I have a couple of questions. I am assuming the provider blocks (1st and 3rd code block) would go in my apps providers array? In the provider block, the line "new TestModel(http, config), deps: [Http, CONFIG];" I get errors, I moved the 'deps' property underneath use factory so it is a property root level of the provider object. Now when I use the code I get this.testModelFactory is not a function – Beamer180 Jul 20 '16 at 15:25
  • the codee shoulg be added into the providers array `bootstrap(AppComponent, [OtherProvider, {provide: ...}])` or into `@Component(... providers: [{provide: ...}])`. `deps` is only an example dependency that you can remove or replace depending on your actual requirements (what needs to be passes to the services constructor9 – Günter Zöchbauer Jul 20 '16 at 15:32
  • 1
    Got it working! I had to make a couple of changes, in the provider object, deps has to be on the root level of the object, not inside the inner function. Also both functions needed to return. so the outer function, the useFactory function needed to return the inner function and the inner function needs to return the new model. – Beamer180 Jul 20 '16 at 15:38
  • Sorry, you are right, I missed that (currently also only on the phone) glad to hear you figured it out. – Günter Zöchbauer Jul 20 '16 at 15:40
  • 1
    I editted your code to what worked for me. Thank you again for you help! – Beamer180 Jul 20 '16 at 15:42
  • Hhhmmm, the dependencies (Http and CONFIG) are undefined in the models. – Beamer180 Jul 20 '16 at 16:49
  • I'll have to try myself when I'm back on my computer. – Günter Zöchbauer Jul 20 '16 at 16:55
  • @GünterZöchbauer can we have new instance only on certain condition/scenario other wise it should probably work as single ton? – k11k2 Jan 27 '22 at 19:25
  • @k11k2 You could provide it using a InjectionToken to have a singleton and provide it by other means using a factory where you inject the token into the factory and decide there if you return the injected singleton or `return new MyService()` instead. You can also provide it globally (root), for a lazy loaded component, or for a component. It all depends on what "certain condition/scenario means exactly". You can also do `new MyService()` anywhere in your code if you don't want the provided instance. – Günter Zöchbauer Jan 29 '22 at 07:24
1

First of all, you are mixing two separate concerns here: one is holding data, which is your TestModel's concern, and another is saving that data, which isn't. This second concern should be implemented in TestService instead, it's its concern to talk to the server, so let it do its job.

Then, angular injectables are intended to be singletons. Quite obvious that data objects are not singletons, so you should not inject them through DI. What registered with DI is intended to be a service working with data objects, not data objects themselves. You can operate data objects directly or create some factory service which will create them for you being itself a singleton. There are plenty of ways to achieve that without DI.

You can find more details about angular2 DI here. It's pretty long but luckily not very complicated.

Alexander Leonov
  • 4,694
  • 1
  • 17
  • 25
1

Thanks to everyone above, This is a working plunker I used. Hope it helps

http://plnkr.co/edit/NxGQoTwaZi9BzDrObzyP

import {Component, NgModule, VERSION, Injectable, Inject} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import {HttpClient} from '@angular/common/http'
import {HttpModule} from '@angular/http'

@Injectable()
export class HttpService{

  token = 'hihaa';
 constructor(){
 } 

 myFunction(value){
 console.log(value)

 }
}


export class Country{
  constructor(value,public httpService: HttpService){

    console.log(value,this);
  }

  classes(){

    this.httpService.myFunction('BGGGG')
  }
}


@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Hello {{name}}</h2>
    </div>
  `,
})
export class App {
  name:string;
  country:any;

  constructor(
    @Inject('CountryFactory') countryFactory
    ) {
    this.name = `Angular! v${VERSION.full}`;
    this.country = countryFactory(3);
    this.country.classes();
  }
}

export let CountryProvider = { provide: 'CountryFactory',
    useFactory: (httpService) => {
      return (value) =>{
        return new Country(value,httpService)
      };
    },
    deps: [HttpService]
  }

@NgModule({
  imports: [ BrowserModule,HttpModule ],
  declarations: [ App ],
  bootstrap: [ App ],
  providers: [
    HttpService,
    CountryProvider

  ]
})
export class AppModule {}
Julien
  • 241
  • 1
  • 3
  • 2
  • 1
    You should provide this as an edit to the post or a comment to your selected answer so people discovering this question can see it. – RyPope Jan 09 '18 at 20:44