1

I am attempting to refactor some AngularJS 1.5 service code to take advantage of classes.

My service definition is as follows:

(() => {
    "use strict";

    class notification {
        constructor($mdToast, $mdDialog, $state) {

            /* injected members */
            this.toast = $mdToast
            this.dialog = $mdDialog
            this.state = $state

            /* properties */
            this.transitioning = false
            this.working = false
        }

        openHelp() {
            this.showAlert({
                "title": "Help",
                "textContent": `Help is on the way!`,
                "ok": "OK"
            })
        }
        showAlert(options) {
            if (angular.isString(options)) {
                var text = angular.copy(options)
                options = {}
                options.textContent = text
                options.title = " "
            }
            if (!options.ok) {
                options.ok = "OK"
            }
            if (!options.clickOutsideToClose) {
                options.clickOutsideToClose = true
            }
            if (!options.ariaLabel) {
                options.ariaLabel = 'Alert'
            }
            if (!options.title) {
                options.title = "Alert"
            }
            return this.dialog.show(this.dialog.alert(options))
        }
        showConfirm(options) {
            if (angular.isString(options)) {
                var text = angular.copy(options)
                options = {}
                options.textContent = text
                options.title = " "
            }
            if (!options.ok) {
                options.ok = "OK"
            }
            if (!options.cancel) {
                options.cancel = "Cancel"
            }
            if (!options.clickOutsideToClose) {
                options.clickOutsideToClose = false
            }
            if (!options.ariaLabel) {
                options.ariaLabel = 'Confirm'
            }
            if (!options.title) {
                options.title = "Confirm"
            }
            return this.dialog.show(this.dialog.confirm(options))
        }
        showToast(toastMessage, position) {
            if (!position) { position = 'top' }
            return this.toast.show(this.toast.simple()
                .content(toastMessage)
                .position(position)
                .action('OK'))
        }
        showYesNo(options) {
            options.ok = "Yes"
            options.cancel = "No"
            return this.showConfirm(options)
        }
        uc() {
            return this.showAlert({
                htmlContent: "<img src='img\\underconstruction.jpg'>",
                ok: "OK",
                title: "Under Construction"
            })
        }

    }
    angular.module('NOTIFICATION', []).service("notification", notification)
})()

The service seems to be created fine, however, when I reference it from a component's controller that it's been injected into inside of the services methods "this" references the controller that the service has been injected into rather than the service. In looking at the controller in the debugger it appears that all of the methods that I have defined for the service actually have been added to the controller.

In the controller, I am essentially mapping some controller methods to methods of the service like so:

function $onInit() {
        Object.assign(ctrl, {
            // Properties        
            title: "FTP Order Processing",
            menuComponent: "appMenu",
            reportsOn: false,
            userName: "",
            notification: notification,
            // working: false,
            // Methods
            closeSideNav: closeSideNav,
            menuHit: menuHit,
            openHelp: notification.openHelp,
            showConfirm: notification.showConfirm,
            showSideNav: showSideNav,
            showAlert: notification.showAlert,
            showToast: notification.showToast,
            showYesNo: notification.showYesNo,
            toggleReports: toggleReports,
            // uc: uc
        })

        Object.defineProperty(ctrl, "working", {
            get: () => { return ctrl.notification.working },
            set: (value) => { ctrl.notification.working = value }
        })
    }

So it makes sense that "this" refers to the controller. When I was using a non-class based service it just didn't matter because I referred to the members of the service within the service using a variable that referenced the service.

So I guess my issue is, how do I refer to members of the service class from within its methods when those methods have been mapped to another object?

Mike Feltman
  • 5,160
  • 1
  • 17
  • 38
  • If you have problems with using it in a controller, please, specify how you use it and what exactly the problem is. See http://stackoverflow.com/help/mcve – Estus Flask May 03 '17 at 13:47
  • The problem is that "this" from within the methods of the service actually evaluates to the controller rather than to the service. OK, wait I reviewed this a little bit to see how I could state this more clearly it may have lead me somewhere. BRB... – Mike Feltman May 03 '17 at 13:48
  • Then please, provide code that shows how this happens. Just saying this doesn't expain anything. http://stackoverflow.com/help/mcve is necessary, the question is considered off-topic otherwise. – Estus Flask May 03 '17 at 14:01
  • Perhaps you're not familiar with BRB? – Mike Feltman May 03 '17 at 14:20
  • I guess I posted the comment before seeing 'BRB'. – Estus Flask May 03 '17 at 14:30

2 Answers2

1

This is something that we do for our classes, ES6 using Angular 1.5+:

import { decorator } from 'app/decorators';
export default class FooClass {
  /* @ngInject */
  constructor(Restangular) {
    this._Restangular = Restangular;
  }

  @decorator
  someMethod(argument_one) {
    return argument_one.property;
  }
}

So it's about the same as yours, slightly different. I left a decorator example in just in case.

rrd
  • 5,789
  • 3
  • 28
  • 36
1

The problem isn't related to ES6 classes (they are just syntactic sugar for ES5 constructor functions) but to JS in general.

When a method is assigned to another object like

foo.baz = bar.baz

foo.baz() will have foo as this - unless bar.baz was bound as bar.baz = bar.baz.bind(baz), or it is ES6 arrow function that has bar as lexical this.

Assigning methods to controlller like that won't work well and will result in having them wrong context.

This can be fixed like

Object.assign(this, {
  ...
  showAlert: notification.showAlert.bind(notification)
});

or

Object.assign(this, {
  ...
  showAlert: (...args) => notification.showAlert(...args)
});

But the good recipe is to just not let service methods lose their context.

A controller should just assign service instance as

this.notification = notification;

and access its methods like this.notification.openHelp() in controller or {{ $ctrl.notification.openHelp() }} in view.

Otherwise using class prototype methods is preferable:

showAlert(options) {
  return this.notification.showAlert(options);
}

Since it exposes the methods on controller prototype, this allows to use inheritance and testing approaches that aren't available for instance methods (also is more effective when a controller is instantiated multiple times).

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Thanks, after digging into it that's what I concluded too. Unfortunately that big of a refactor isn't in scope at present. I tried this just for grins in one controller where this service is used and either solution does work, so I'm going to accept this answer. – Mike Feltman May 03 '17 at 14:45
  • A cleaner approach that is idiomatic to ES6 OOP is to have controller prototype methods for the methods you want to wrap on controller, `showAlert(options) { return this.notification.showAlert(options) }`. This is what prototype methods are for, `Object.assign` may be ok for instance properties but it doesn't look very good for methods. – Estus Flask May 03 '17 at 14:51
  • The controllers all still function based in this case. Eventually when this app moves stats to move to ng4 that will be an option. Right now I'm just trying to prep code that needs to be touched to be more ready for ng4. The use of Object.assign for the methods is just syntactic sugar used with the revealing module pattern for now. – Mike Feltman May 03 '17 at 14:56
  • 1
    If you have plans to update to A4, this may be even more important. Object.assign is basically mixin technique which will have some drawbacks, while sticking to prototype methods is a safe bet, design-wise. You can check [this answer](http://stackoverflow.com/a/43601993/3731501) for some points on instance vs prototype methods. – Estus Flask May 03 '17 at 15:10