Mike Feltman
Mike Feltman

Reputation: 5176

Using ES6 Classes for services in AngularJS 1.5

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?

Upvotes: 1

Views: 2592

Answers (2)

Estus Flask
Estus Flask

Reputation: 222369

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).

Upvotes: 1

rrd
rrd

Reputation: 5957

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.

Upvotes: 1

Related Questions