diff --git a/app/assets/javascripts/discourse/app/components/modal-container.hbs b/app/assets/javascripts/discourse/app/components/modal-container.hbs
index d081003cb8f..24ad1d2563b 100644
--- a/app/assets/javascripts/discourse/app/components/modal-container.hbs
+++ b/app/assets/javascripts/discourse/app/components/modal-container.hbs
@@ -1,11 +1,14 @@
-{{#if this.modal.modalBodyComponent}}
-
+{{#if this.modal.activeModal}}
+ {{#each (array this.modal.activeModal) as |activeModal|}}
+ {{! #each ensures that the activeModal component/model are updated atomically }}
+
+ {{/each}}
{{/if}}
{{! Legacy modals depend on this wrapper being in the DOM at all times. Eventually this will be dropped.
diff --git a/app/assets/javascripts/discourse/app/services/modal.js b/app/assets/javascripts/discourse/app/services/modal.js
index 5a10dd5b812..6cd7f4d3c59 100644
--- a/app/assets/javascripts/discourse/app/services/modal.js
+++ b/app/assets/javascripts/discourse/app/services/modal.js
@@ -20,10 +20,10 @@ const LEGACY_OPTS = new Set([
@disableImplicitInjections
class ModalService extends Service {
- @tracked modalBodyComponent;
+ @tracked activeModal;
@tracked opts = {};
+
@tracked containerElement;
- #resolveShowPromise;
@action
setContainerElement(element) {
@@ -42,12 +42,13 @@ class ModalService extends Service {
show(modal, opts) {
this.close({ initiatedBy: CLOSE_INITIATED_BY_MODAL_SHOW });
+ let resolveShowPromise;
const promise = new Promise((resolve) => {
- this.#resolveShowPromise = resolve;
+ resolveShowPromise = resolve;
});
this.opts = opts || {};
- this.modalBodyComponent = modal;
+ this.activeModal = { component: modal, opts, resolveShowPromise };
const unsupportedOpts = Object.keys(opts).filter((key) =>
LEGACY_OPTS.has(key)
@@ -64,8 +65,8 @@ class ModalService extends Service {
}
close(data) {
- this.#resolveShowPromise?.(data);
- this.#resolveShowPromise = this.modalBodyComponent = null;
+ this.activeModal?.resolveShowPromise?.(data);
+ this.activeModal = null;
this.opts = {};
}
}
@@ -240,6 +241,6 @@ export default class ModalServiceWithLegacySupport extends ModalService {
}
get isLegacy() {
- return this.name && !this.modalBodyComponent;
+ return this.name && !this.activeModal;
}
}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/modal-service-test.js b/app/assets/javascripts/discourse/tests/acceptance/modal-service-test.js
index 2c3c48c3f25..4390cc9b246 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/modal-service-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/modal-service-test.js
@@ -109,5 +109,44 @@ acceptance("Modal service: component-based API", function () {
);
});
+ test("lifecycle hooks and arguments", async function (assert) {
+ await visit("/");
+
+ const events = [];
+
+ class ModalWithLifecycleHooks extends MyModalClass {
+ constructor() {
+ super(...arguments);
+ events.push(`constructor: ${this.args.model?.data}`);
+ }
+
+ willDestroy() {
+ events.push(`willDestroy: ${this.args.model?.data}`);
+ }
+ }
+
+ const modalService = getOwner(this).lookup("service:modal");
+
+ modalService.show(ModalWithLifecycleHooks, {
+ model: { data: "argumentValue" },
+ });
+ await settled();
+
+ assert.deepEqual(
+ events,
+ ["constructor: argumentValue"],
+ "constructor called with args available"
+ );
+
+ modalService.close();
+ await settled();
+
+ assert.deepEqual(
+ events,
+ ["constructor: argumentValue", "willDestroy: argumentValue"],
+ "constructor called with args available"
+ );
+ });
+
// (See also, `tests/integration/component/d-modal-test.js`)
});