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`) });