FIX: DEV: Introduce `@bind` decorator (#10439)

FIX: DEV: Introduce @bind decorator (#10439)

Fixes a bug in controllers/insert-hyperlink where addEventListener was called with different (anonymous) functions than the matching removeEventListener calls.

diff --git a/app/assets/javascripts/discourse-common/addon/utils/decorators.js b/app/assets/javascripts/discourse-common/addon/utils/decorators.js
index 2b6548e..ebed6e1 100644
--- a/app/assets/javascripts/discourse-common/addon/utils/decorators.js
+++ b/app/assets/javascripts/discourse-common/addon/utils/decorators.js
@@ -4,6 +4,7 @@ import extractValue from "discourse-common/utils/extract-value";
 import decoratorAlias from "discourse-common/utils/decorator-alias";
 import macroAlias from "discourse-common/utils/macro-alias";
 import { schedule, next } from "@ember/runloop";
+import { bind as emberBind } from "@ember/runloop";
 
 export default function discourseComputedDecorator(...params) {
   // determine if user called as @discourseComputed('blah', 'blah') or @discourseComputed
@@ -29,6 +30,22 @@ export function afterRender(target, name, descriptor) {
   };
 }
 
+export function bind(target, name, descriptor) {
+  return {
+    configurable: true,
+    get() {
+      const bound = emberBind(this, descriptor.value);
+      const attributes = Object.assign({}, descriptor, {
+        value: bound
+      });
+
+      Object.defineProperty(this, name, attributes);
+
+      return bound;
+    }
+  };
+}
+
 export function readOnly(target, name, desc) {
   return {
     writable: false,
diff --git a/app/assets/javascripts/discourse/app/components/d-document.js b/app/assets/javascripts/discourse/app/components/d-document.js
index ec022e8..2cbe7e8 100644
--- a/app/assets/javascripts/discourse/app/components/d-document.js
+++ b/app/assets/javascripts/discourse/app/components/d-document.js
@@ -1,9 +1,8 @@
 import Component from "@ember/component";
-import { bind } from "@ember/runloop";
 import { inject as service } from "@ember/service";
+import { bind } from "discourse-common/utils/decorators";
 
 export default Component.extend({
-  _boundFocusChange: null,
   tagName: "",
   documentTitle: service(),
 
@@ -11,10 +10,9 @@ export default Component.extend({
     this._super(...arguments);
 
     this.documentTitle.setTitle(document.title);
-    this._boundFocusChange = bind(this, this._focusChanged);
-    document.addEventListener("visibilitychange", this._boundFocusChange);
-    document.addEventListener("resume", this._boundFocusChange);
-    document.addEventListener("freeze", this._boundFocusChange);
+    document.addEventListener("visibilitychange", this._focusChanged);
+    document.addEventListener("resume", this._focusChanged);
+    document.addEventListener("freeze", this._focusChanged);
     this.session.hasFocus = true;
 
     this.appEvents.on("notifications:changed", this, this._updateNotifications);
@@ -23,10 +21,9 @@ export default Component.extend({
   willDestroyElement() {
     this._super(...arguments);
 
-    document.removeEventListener("visibilitychange", this._boundFocusChange);
-    document.removeEventListener("resume", this._boundFocusChange);
-    document.removeEventListener("freeze", this._boundFocusChange);
-    this._boundFocusChange = null;
+    document.removeEventListener("visibilitychange", this._focusChanged);
+    document.removeEventListener("resume", this._focusChanged);
+    document.removeEventListener("freeze", this._focusChanged);
 
     this.appEvents.off(
       "notifications:changed",
@@ -46,6 +43,7 @@ export default Component.extend({
     );
   },
 
+  @bind
   _focusChanged() {
     if (document.visibilityState === "hidden") {
       if (this.session.hasFocus) {
diff --git a/app/assets/javascripts/discourse/app/components/global-notice.js b/app/assets/javascripts/discourse/app/components/global-notice.js
index ced8c86..e76a368 100644
--- a/app/assets/javascripts/discourse/app/components/global-notice.js
+++ b/app/assets/javascripts/discourse/app/components/global-notice.js
@@ -1,6 +1,6 @@
 import getURL from "discourse-common/lib/get-url";
+import { bind } from "discourse-common/utils/decorators";
 import I18n from "I18n";
-import { bind, cancel } from "@ember/runloop";
 import Component from "@ember/component";
 import LogsNotice from "discourse/services/logs-notice";
 import EmberObject, { computed } from "@ember/object";
@@ -197,22 +197,16 @@ export default Component.extend({
   },
 
   _setupObservers() {
-    this._boundLogsNoticeHandler = bind(this, this._handleLogsNoticeUpdate);
-    LogsNotice.current().addObserver("hidden", this._boundLogsNoticeHandler);
-    LogsNotice.current().addObserver("text", this._boundLogsNoticeHandler);
+    LogsNotice.current().addObserver("hidden", this._handleLogsNoticeUpdate);
+    LogsNotice.current().addObserver("text", this._handleLogsNoticeUpdate);
   },
 
   _tearDownObservers() {
-    if (this._boundLogsNoticeHandler) {
-      LogsNotice.current().removeObserver("text", this._boundLogsNoticeHandler);
-      LogsNotice.current().removeObserver(
-        "hidden",
-        this._boundLogsNoticeHandler
-      );
-      cancel(this._boundLogsNoticeHandler);
-    }
+    LogsNotice.current().removeObserver("text", this._handleLogsNoticeUpdate);
+    LogsNotice.current().removeObserver("hidden", this._handleLogsNoticeUpdate);
   },
 
+  @bind
   _handleLogsNoticeUpdate() {
     const logNotice = Notice.create({
       text: LogsNotice.currentProp("message"),
diff --git a/app/assets/javascripts/discourse/app/components/pwa-install-banner.js b/app/assets/javascripts/discourse/app/components/pwa-install-banner.js
index 1dd4bef..19d1a30 100644
--- a/app/assets/javascripts/discourse/app/components/pwa-install-banner.js
+++ b/app/assets/javascripts/discourse/app/components/pwa-install-banner.js
@@ -1,13 +1,13 @@
-import { bind } from "@ember/runloop";
 import Component from "@ember/component";
-import discourseComputed, { on } from "discourse-common/utils/decorators";
+import discourseComputed, { bind, on } from "discourse-common/utils/decorators";
 
 const USER_DISMISSED_PROMPT_KEY = "dismissed-pwa-install-banner";
 
 export default Component.extend({
   deferredInstallPromptEvent: null,
 
-  _handleInstallPromptEvent(event) {
+  @bind
+  _onInstallPrompt(event) {
     // Prevent Chrome 76+ from automatically showing the prompt
     event.preventDefault();
     // Stash the event so it can be triggered later
@@ -16,13 +16,12 @@ export default Component.extend({
 
   @on("didInsertElement")
   _registerListener() {
-    this._promptEventHandler = bind(this, this._handleInstallPromptEvent);
-    window.addEventListener("beforeinstallprompt", this._promptEventHandler);
+    window.addEventListener("beforeinstallprompt", this._onInstallPrompt);
   },
 
   @on("willDestroyElement")
   _unregisterListener() {
-    window.removeEventListener("beforeinstallprompt", this._promptEventHandler);
+    window.removeEventListener("beforeinstallprompt", this._onInstallPrompt);
   },
 
   @discourseComputed
diff --git a/app/assets/javascripts/discourse/app/components/share-popup.js b/app/assets/javascripts/discourse/app/components/share-popup.js
index 7668f1f..580a6c9 100644
--- a/app/assets/javascripts/discourse/app/components/share-popup.js
+++ b/app/assets/javascripts/discourse/app/components/share-popup.js
@@ -1,10 +1,10 @@
 import I18n from "I18n";
 import { isEmpty } from "@ember/utils";
-import { bind, scheduleOnce, later } from "@ember/runloop";
+import { scheduleOnce, later } from "@ember/runloop";
 import Component from "@ember/component";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
 import { longDateNoYear } from "discourse/lib/formatter";
-import discourseComputed, { on } from "discourse-common/utils/decorators";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
 import Sharing from "discourse/lib/sharing";
 import { nativeShare } from "discourse/lib/pwa-utils";
 import { alias } from "@ember/object/computed";
@@ -94,6 +94,7 @@ export default Component.extend({
     scheduleOnce("afterRender", this, this._focusUrl);
   },
 
+  @bind
   _mouseDownHandler(event) {

[... diff too long, it was truncated ...]

GitHub sha: 7cc5c5bb

This commit appears in #10439 which was approved by jjaffeux and eviltrout. It was merged by CvX.