Reputation: 5176
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
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
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