diff --git a/app/assets/javascripts/discourse/app/components/user-tip.gjs b/app/assets/javascripts/discourse/app/components/user-tip.gjs index be079382a50..847028ebfda 100644 --- a/app/assets/javascripts/discourse/app/components/user-tip.gjs +++ b/app/assets/javascripts/discourse/app/components/user-tip.gjs @@ -4,6 +4,7 @@ import { schedule } from "@ember/runloop"; import { service } from "@ember/service"; import { modifier } from "ember-modifier"; import UserTipContainer from "discourse/components/user-tip-container"; +import helperFn from "discourse/helpers/helper-fn"; import escape from "discourse-common/lib/escape"; import { iconHTML } from "discourse-common/lib/icon-library"; import I18n from "discourse-i18n"; @@ -14,7 +15,7 @@ export default class UserTip extends Component { @service userTips; @service tooltip; - registerTip = modifier(() => { + registerTip = helperFn((_, on) => { const tip = { id: this.args.id, priority: this.args.priority ?? 0, @@ -22,9 +23,9 @@ export default class UserTip extends Component { this.userTips.addAvailableTip(tip); - return () => { + on.cleanup(() => { this.userTips.removeAvailableTip(tip); - }; + }); }); tip = modifier((element) => { @@ -82,10 +83,9 @@ export default class UserTip extends Component { } } diff --git a/app/assets/javascripts/discourse/app/helpers/helper-fn.js b/app/assets/javascripts/discourse/app/helpers/helper-fn.js new file mode 100644 index 00000000000..946e15dc654 --- /dev/null +++ b/app/assets/javascripts/discourse/app/helpers/helper-fn.js @@ -0,0 +1,50 @@ +import Helper from "@ember/component/helper"; +import { registerDestructor } from "@ember/destroyable"; +import { bind } from "discourse-common/utils/decorators"; + +/** + * Build an Ember helper with cleanup logic. The passed function will be called with the named argument, + * and an 'on' utility object which allows you to register a cleanup function via `on.cleanup(...)`. + * + * Whenever any autotracked state is changed, the cleanup function will be run, and your function + * will be re-evaluated. + * + * @param {(args: object, on: { cleanup: () => void } ) => any} fn - The helper function. + */ +export default function helperFn(callback) { + return class extends Helper { + cleanupFn = null; + + constructor() { + super(...arguments); + registerDestructor(this, this.cleanup); + } + + compute(positional, named) { + if (positional.length) { + throw new Error( + "Positional arguments are not permitted for helperFn-defined helpers. Use named arguments instead." + ); + } + + this.cleanup(); + + const on = { + cleanup: (fn) => { + if (this.cleanupFn) { + throw new Error("on.cleanup can only be called once"); + } + this.cleanupFn = fn; + }, + }; + + return callback(named, on); + } + + @bind + cleanup() { + this.cleanupFn?.(); + this.cleanupFn = null; + } + }; +}