1

I have a unique situation and I find myself quite stuck. I have a standard typescript class (.ts) extension, that needs to use methods from an angular2 service. So far I can't see a way to achieve this.

I'm going to try an explain this as simply as I possibly can, as my code base is currently quite complex.

The service in question is operation.service.ts:

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import { Operation } from './operation';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Injectable()
export class OperationService {

  private operationCatalogue: Operation[] = [];
  private url = './src/app/operation-catalogue/operations.json';

  constructor(private http:Http) {
    this.init();
  }

  init(){
      this.fetchOperations().subscribe(res => {
        var i;
        for(i in res["operations"]){
          this.operationCatalogue[i] = new Operation(res["operations"][i]["name"],
                                                     res["operations"][i]["description"],
                                                     res["operations"][i]["iconName"],
                                                     res["operations"][i]["inputs"],
                                                     res["operations"][i]["outputs"]
                                                 );
        }
      });
  }

  getOperations(){
    return this.operationCatalogue;
  }

  fetchOperations(): Observable<Operation[]>{
      return this.http.get(this.url).map(res => res.json());
  }

  /*
  Get a single operation from the operation catalogue.
  */
  getOperation(name: String): Operation {
    var theOne;
    for (var i = 0; i < this.operationCatalogue.length; i++){
        if (this.operationCatalogue[i].name === name){
            theOne = this.operationCatalogue[i];
        }
    }
    return theOne;
  }
}

You'll see from this code that the service builds an of these Operation objects from data found in a locally sourced .json file. In production, my application will get the data from a server rather than a local file.

The important method to note is getOperation() which takes a string and returns an Operation object from the array.

This service is a key part of my application, and is used to populate a HTML list in another component.

Now, I have another class, called WSJSerializer. Put simply, and just to give you some context, this class takes a JSON string which represents an array of Operation objects that were found on disk and turns them into Operation objects. In order to do this, WSJSerializer needs to use the getOperation() method of OperationService.

WSJSerializer.ts:

export class WSJDeserializer {

    private jsonString : string;
    private parsedString : Object;

    constructor(string : String){
        this.jsonString = <string> string;
        this.parsedString = JSON.parse(this.jsonString);
    }

    public deserialize() {
        var rootOp = this.parsedString["operation"];
        var childOps = rootOp["operations"];
        var numOps = childOps.length;

        for (var i = 0; i < numOps; i++){
            var childOp = childOps[i]["operation"];
            this.deserializeObject(childOp);
        }
    }

    private deserializeObject(operation : Object){
        //NEED TO USE THE getOperation() METHOD HERE!!!!
    }
}

This is the crux of my problem. I'm unable to interface with OperationService from WSJSerializer, I'm guessing it's some sort of angular restriction. I tried to import OperationService into WSJSerializer and then call the getOperation() method, but I got the following error:

WSJDeserializer.ts (25,26): Property 'getOperations' does not exist on type 'typeof OperationService'.)

How can I call the getOperation() method of OperationService from within WSJSerializer? If it's not directly possible, can I write some sort of interface (not an OO-concept interface) or helper class to solve the problem? Is there some other workaround?

Other information:

  • WSJSerializer is used in the root angular component of my application but OperationService is not.
  • OperationService is used in a child component of the root component, of which the root component has direct access to via a ViewChild().
  • WSJSerializer is not a service because it needs to directly modify other angular components by calling their methods.

Root component where WSJSerializer is used:

import { WSJDeserializer } from './serialization/WSJDeserializer';

//services...
import { SerializationService } from './serialization/serialization.service';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    providers: [SerializationService]
})
export class AppComponent implements OnInit {

    @ViewChild(PixiComponent) private pixiComponent : PixiComponent;
    @ViewChild(LogComponent) private logComponent : LogComponent;
    @ViewChild(EditGlobalNamesDialogBoxComponent) private globalNamesEditDialog : EditGlobalNamesDialogBoxComponent;
    @ViewChild(GlobalNamesComponent) private globalNamesComponent : GlobalNamesComponent;

    private globalNamesController : GlobalNamesController;

    ngOnInit() {
        this.globalNamesController = new GlobalNamesController(this.globalNamesEditDialog, this.globalNamesComponent, this.pixiComponent);
    }

    public leftMousedown(){
        this.pixiComponent.clickFromOutside();
    }

    public rightMousedown($event){
        $event.preventDefault();
        this.pixiComponent.clickFromOutside();
    }

    //TODO: Escape key functionality
    notifyEscapePressed(){

    }

    /*------------------------------------------------------------------------*/
    // Following method for log functionality

    public logData($event){
        this.logComponent.logText($event);
    }

    /*------------------------------------------------------------------------*/

    /*------------------------------------------------------------------------*/
    // Following methods for global names functionality 
    // handled by this.globalNamesController

    public globalNameEditRequest(data : EditGlobalNameRequestData){
        this.globalNamesController.displayEditGlobalNamesDialogBox(data);
    }

    public setNewGlobalName(data : EditGlobalNameRequestData) {
        this.globalNamesController.setNewGlobalName(data);
    }

    /*------------------------------------------------------------------------*/

    /*------------------------------------------------------------------------*/
    // Deserialization...

    public deserialize($event){
        var workspaceString : String = $event;
        var deserializer : WSJDeserializer = new WSJDeserializer(workspaceString)
        this.pixiComponent.resetCanvas();
        deserializer.deserialize();
    }

    /*------------------------------------------------------------------------*/
}
11
  • Did you try adding OperationService as a dependency in your WSJSerializer constructor? Commented Apr 24, 2017 at 1:45
  • @Saravana I tried injecting OperationService into WSJSerializer like this: constructor(@Inject(OperationService) private operationService), but when I try to instantiate WSJSerializer, it gives me an error because it expects an OperationService object to be passed in, even though it's being injected... Commented Apr 24, 2017 at 1:49
  • The question misses the obvious important part - how WSJSerializer is used and why it's not a service. Commented Apr 24, 2017 at 1:52
  • @estus The reason WSJSerializer is not a service is because it needs to directly modify other angular components - which is not what services should be used for. I will edit my question to add this information. Commented Apr 24, 2017 at 1:54
  • Services are to make use of Angular DI, no more, no less. Please, provide the code for the components where WSJSerializer is used. Commented Apr 24, 2017 at 2:15

2 Answers 2

2

In its current state WSJSerializer has to make use of DI pattern (not Angular DI) in order to get an instance of OperationService:

export class WSJDeserializer {
    ...
    constructor(private operationService: OperationService, string : String){
        this.jsonString = <string> string;
        this.parsedString = JSON.parse(this.jsonString);
    }
    ...
    private deserializeObject(operation : Object){
        this.operationService.getOperation(...);
        ...
    }
}

Which should be passed from an injectable that instantiates this class, i.e. a component:

new WSJDeserializer(workspaceString, this.operationService)

This looks like a design issue and the proper refactoring solution may depend on the other factors that weren't explained in the question.

One possibility is that wrong things was taken as entities for the class. Is there a real need for an object that holds state (parsedString) and is able to deserialize it more than once? If there isn't then serializer can become singleton injectable that accepts a string as an argument for its methods:

@Injectable()
export class WSJDeserializer {
    constructor(private operationService: OperationService) {}

    deserialize(string : String) {
            ...
            this.deserializeObject(string , childOp);
            ...
    }

    private deserializeObject(string : String, operation : Object){
        this.operationService.getOperation(...);
        ...
    }
}

Another possibility is that the entities are right, but there's a real need to have an instance that holds state, so one class is not enough. Singleton injectable should exist any way in order to get OperationService. So there should be two classes instead of one:

export class WSJDeserializable {
    ...
    constructor(string : String){
        this.jsonString = <string> string;
        this.parsedString = JSON.parse(this.jsonString);
    }
}

@Injectable()
export class WSJDeserializer {
    constructor(private operationService: OperationService) {}

    deserialize(deserializableObj: WSJDeserializable) {
            ...
            this.deserializeObject(deserializableObj, childOp);
            ...
    }
    ...
}

Which should be passed from a component:

const deserializableObj = new WSJDeserializable(...);
deserializer.deserialize(deserializableObj);

There are more options to do this, e.g. WSJDeserializer instance can have factory method that creates WSJDeserializable instances, but in the end it stays a singleton, because Angular services are singletons (within a single injector).

Sign up to request clarification or add additional context in comments.

2 Comments

This was very helpful, thank you. Your suggestion to not save state of the string is indeed what the solution called for.
You're welcome. I guess this is a common design problem.
0

So, you need an instance of OperationService inside of WSJSerializer? There is only one way to do that and that is through the injector. In order to make WSJSerializer injectable, you must turn it into a service.

Well, that is a little bit of a lie. There is another way. If you can't make WSJSerializer a service, then you will also need to ensure that OperationService is also not a service. This is fairly easy to do. Instead of using the angular Http service, you can use native libraries instead (either XmlHttpRequest or fetch (note that fetch only works on modern browsers).

2 Comments

Assuming I decide to turn WSJSerializer into a service, how can I get an instance of OperationService into it?
You add @Injectable() to the class declaration and in the class constructor, add a parameter of type OperationService. Then add the WSJSerializer as a provider in your component.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.