Search code examples
javascriptangularjsvue.jsngresource

Stuck converting ngResource angular service to Vanilla JS


We are migrating our site from old angularjs to using Vuejs. Step one is to modify our services used throughout the site which all rely heavily on ngResource and convert them into vanilla js code that can be called by Vue.

The challenge is that in addition to making API calls using ngResource they also extending the returning object via prototype. While using a the module pattern in regular javascript I can mimick the API behaviour of the ngResource service. But I am not clear how to set this up so that it can also support the prototype extensions that are being applied to the returning object (whether a single object or an array).

For example one of our current services might look like this

        "use strict";
    
    angular.module("myApp")
        .factory("PortfolioService",
            [
                "$resource", "$rootScope", "$http",
                function($resource,
                    $rootScope,
                    $http) {
                    var Portfolio = $resource("services/portfolios/:Uid",
                        {
                            '_': function() { return Date.now() }
                        }, {
                   'query': {
                        method: "GET",
                        url: "services/portfolios/",
                        transformResponse: $http.defaults.transformResponse.concat([
                            function (data) { return data.Data; }
                        ])
                    }
                });
                      
                    Portfolio.prototype.getPicUrl= function() {
                        return this.ImgBasePath + this.ImgUrl;
                    };
                  return Portfolio;
            }
        ]);

Note: that it mames a service call called query but also extends the returning object with a new function called getPicUrl.

I have created a JS equivalent that looks like this

const vPortfolioService = (() => {
    var baseapipath = "http://localhost:8080/services/";

    var Portfolio = {
        query: function() {
            return axios.get(baseapipath + "portfolios/");
        }
    };

    Portfolio.prototype.getPicUrl= function () {
         return this.ImgBasePath + this.ImgUrl;
    }

    return Portfolio;
})();

The service part works fine but I dont know how to do what ngResource seems to do which is to return a resource from the API which includes the prototype extensions.

Any advice would be appreciated. Thanks


Solution

  • As I mentioned in my replies to @Igor Moraru, depending on how much of your code base you're replacing, and how much of that existing code base made use of the full capabilities of ngResource, this is not a trivial thing to do. But just focusing on the specific example in your question, you need to understand some vanilla JS a bit more first.

    Why does the Portfolio object have a prototype property when it's returned from $resource(), but not when it's created via your object literal? Easy: The object returned by $resource() is a function, which in turn also means it's a class, and those have a prototype property automatically.

    In JavaScript, regular functions and classes are the same thing. The only difference is intent. In this case, the function returned by $resource() is intended to be used as a class, and it's easy to replicate certain aspects of that class such as the static query method and the non-static (i.e., on the prototype) getPicUrl method:

    const vPortfolioService = (() => {
        var baseapipath = "http://localhost:8080/services/";
    
        class Portfolio {
            constructor(params) {
                Object.assign(this, params);
            }
            
            static query() {
                return axios.get(baseapipath + "portfolios/").then(res => {
                    // this convert the objects into an array of Portfolio instances
                    // you probably want to check if the response is valid before doing this...
                    return res.data.map(e => new this(e));
                });
            }
            
            getPicUrl() {
                return this.ImgBasePath + this.ImgUrl;   
            }
        }
    
        return Portfolio;
    })();
    

    But the problem is, this probably isn't enough. If you're migrating/refactoring an entire application, then you have to be certain of every instance in which your application uses ngResource, and based on your question, I'm fairly certain you've used it more than this class would allow.

    For example, every class created by $resource also has static methods such as get, post, etc., as well as corresponding instance methods such as $get, $post, and so on. In addition, the constructor I've provided for the class is just a very lazy stop-gap to allow you to create an instance with arbitrary properties, such as the properties referenced by the getPicUrl method.

    So I think you have three options:

    • Continue playing with the above class to fit something closer to what you need, and then edit every place in your application where your code relies on the Portfolio Service so that it now reflects this new, more limited class. This is probably your best option, especially if your application isn't that big and you don't have to worry about someone else's code not working
    • Analyze the source code for ngResource, rip it out, and modify it so it doesn't need AngularJS to work. Perhaps someone has already done this and made it available as a library? Kind of a long shot I'd guess, but it may work.
    • Keep AngularJS in your application, alongside Vue, but only use it to grab the essentials like $http and $resource. An example implementation is below, but this is probably the worst option. There's additional overhead by bootstrapping pieces of angular, and tbh it probably needs to bootstrap other pieces I haven't thought of... but it's neat I guess lol:
    const vPortfolioService = (() => {
        var inj = angular.injector(["ng", "ngResource"]);
        var $http = inj.get("$http"), $resource = inj.get("$resource");
    
        var Portfolio = $resource("services/portfolios/:Uid",
            {
                '_': function () { return Date.now() }
            }, {
            'query': {
                method: "GET",
                url: "services/portfolios/",
                transformResponse: $http.defaults.transformResponse.concat([
                    function (data) { return data.Data; }
                ])
            }
        });
    
        Portfolio.prototype.getPicUrl = function () {
            return this.ImgBasePath + this.ImgUrl;
        };
        
        return Portfolio;
    })();