2

I'm trying to cleanly write an Angular custom $resource extension as a factory as a TypeScript class using DefinatelyTyped (IResource, IResourceClass and friends).

According to Misko Hevery resources are just constructor functions so I was expecting to be able to define my $resource as a regular class with some typesafe interfaces (INamedEntityResource or INamedEntity) and mixin the service definition but I can't seem to get the standard class methods on my NamedEntityResource prototype to end up on factory instances.

Is there a way of doing this with the constructor() function or should I give up and just define the service in plain JavaScript?

declare module EntityTypes {
    interface INamedEntity { }
}

module Services {

    export interface INamedEntitySvc {
        Name(params: {}, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
        Clear(params: {}, value: EntityTypes.INamedEntity, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
    }

    // WILL have correct interface definition for the resource
    export interface INamedEntityResource extends NamedEntityResource, INamedEntitySvc { }

    export class NamedEntityResource {

        // #1 DOESN'T WORK - These are on NamedEntityResource.prototype but don't end up on svc
        public someMethod() { }
        public someOtherMethod() { }

        constructor($resource) {
            var paramDefaults = {
            };

            var svc: INamedEntitySvc = $resource(getUrl(), paramDefaults, {
                Name: <any>{ method: "GET", params: { action: "Name" } },
                Clear: <any>{ method: "PATCH", params: { action: "Clear" }, headers: { 'Content-Type': 'application/json' } },
            });

            // THIS WORKS - but it's not a NamedEntityResource
            svc["prototype"].someMethod = function () { }
            svc["prototype"].someOtherMethod = function () { }
            return <any>svc;

            // #1 DOESN'T WORK THOUGH
            return; // doesn't pick up methods on prototype

            // #2 THIS DOESN'T WORK EITHER
            NamedEntityResource["prototype"] = angular.extend(this["prototype"] || {}, svc["prototype"]);
            return this;
        }
    }

    // Registration
    var servicesModule: ng.IModule = angular.module('npApp.services');
    servicesModule.factory('NamedEntityResource', NamedEntityResource);
}

Further

So The purpose of this is to allow me to write a resource class{} with methods that will be annotated on every resource I load over HTTP. In this case, my INamedEntitys.

This is the closest solution I've been able to get so far which does appear to work, but it feels really nasty.

module Services {

    export interface INamedEntitySvc {
        Name(params: {}, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
        Clear(params: {}, value: EntityTypes.INamedEntity, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
    }

    // WILL have correct interface definition for the resource
    export interface INamedEntityResource extends NamedEntityResource, INamedEntitySvc { }

    export class NamedEntityResourceBase {
        public someMethod() { }
        public someOtherMethod() { }
    }

    // extend our resource implementation so that INamedEntityResource will have all the relevant intelisense
    export class NamedEntityResource extends NamedEntityResourceBase {

        constructor($resource) {
            super(); // kind of superfluous since we're not actually using this instance but the compiler requires it

            var svc: INamedEntitySvc = $resource(getUrl(), { }, {
                Name: <any>{ method: "GET", params: { action: "Name" } },
                Clear: <any>{ method: "PATCH", params: { action: "Clear" }, headers: { 'Content-Type': 'application/json' } },
            });

            // Mixin svc definition to ourself - we have to use a hoisted base class because this.prototype isn't setup yet
            angular.extend(svc["prototype"], NamedEntityResourceBase["prototype"]);

            // Return Angular's service (NOT this instance) mixed in with the methods we want from the base class
            return <any>svc;
        }

        thisWontWork() {
            // since we never actually get a NamedEntityResource instance, this method cannot be applied to anything.
            // any methods you want have to go in the base prototype
        }
    }

    // Registration
    var servicesModule: ng.IModule = angular.module('npApp.services');
    servicesModule.factory('NamedEntityResource', NamedEntityResource);
}

The trick was to;

  1. Hoist the methods I want on the service up into a base class because this.prototype isn't initialised by the time my constructor() function is called.
  2. Return svc which is the angular $resource service from the constructor, which you can do in JavaScript of course, but it feels like really dirty duck-typing in TypeScript.
  3. In order to get the methods on svc.prototype I extend that directly from my base class. This is particularly nasty as it means setting up the prototype every time an instance is created.
  4. The final pungent aroma to this sh** sandwich is I have to call super() on the constructor for the instance I'm throwing away just to get it to compile.

However, at the end of all that, I can add methods to NamedEntityResourceBase and they'll appear in the prototype of all entities loaded from my HTTP resource.

3 Answers 3

3

I was looking for the answer to this a while. And it was in the typescript documentation. A interface can extend a class. The solution to adding methods to a instance of a resource is below:

class Project {
    id: number;
    title: string;

    someMethod(): boolean {
        return true;
    }
}

export interface IProject extends ng.resource.IResource<IProject>, Project {
    // here you add any method interface generated by the $resource
    // $thumb(): angular.IPromise<IProject>;
    // $thumb(params?: Object, success?: Function, error?: Function): angular.IPromise<IProject>;
    // $thumb(success: Function, error?: Function): angular.IPromise<IProject>;
}

export interface IProjectResourceClass extends ng.resource.IResourceClass<IProject> { }

function projectFactory($resource: ng.resource.IResourceService): IProjectResourceClass {
    var Resource = $resource<IProject>('/api/projects/:id/', { id: '@id' });

    // the key, for this to actually work when compiled to javascript
    angular.extend(Resource.prototype, Project.prototype);
    return Resource;
}
module projectFactory {
    export var $inject: string[] = ['$resource'];
}

I have not fully tested, but I have tested a bit and works.

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

3 Comments

Resource instance methods such as $save are removed if you overwrite prototype. You can fix it by using angular.extend(Resource.prototype, Project.prototype); instead.
This is awesome and works for me. Do you know how to do this for the error type as well? In Typescript, the error type of $resource is angular.IHttpPromiseCallbackArg<T>, but I'm not sure where in IProject (or elsewhere) we'd do an equivalent angular.extend() to add methods to $resource's error type.
Does this add additional properties to the resource instance as well? It seems that the properties would be a part of the resource class..Cant say the same about resource instance..
0

Register classes with service instead of factory:

servicesModule.service('NamedEntityResource', NamedEntityResource);

Disclaimer: my video about additional info you might find useful about service registration in angularjs + typescript : http://www.youtube.com/watch?v=Yis8m3BdnEM&hd=1

1 Comment

Thanks, but I'm not clear how that helps me extend $resource. Do you have an example?
0

here is how i do i am using here $http

module portal{

  var app =angular.module('portal',[]);
  app.service(services);
}

module portal.services {


export class apiService {


    public getData<T>(url?:string): ng.IPromise<T> {

        var def = this.$q.defer();
        this.$http.get(this.config.apiBaseUrl + url).then((successResponse) => {

            if(successResponse)
                def.resolve(successResponse.data);
            else
                def.reject('server error');

        }, (errorRes) => {

            def.reject(errorRes.statusText);
        });

        return def.promise;
    }

    public postData<T>(formData: any, url?:string,contentType?:string): ng.IPromise<T>{

        var def = this.$q.defer();

        this.$http({
            url: this.config.apiBaseUrl + url,
            method: 'POST',
            data:formData,
            withCredentials: true,
            headers: {
                'Content-Type':contentType || 'application/json'
            }
        }).then((successResponse)=>{
            def.resolve(successResponse.data);
        },(errorRes)=>{
            def.reject(errorRes);
        });

        return def.promise;

    }

    static $inject = ['$q','$http', 'config'];

    constructor(public $q:ng.IQService,public $http:ng.IHttpService, public config:interfaces.IPortalConfig) {


    }

}



}

Comments

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.