diff --git a/UPGRADING.md b/UPGRADING.md index 2d1e6068..1c8cd1aa 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,15 @@ # Upgrading ember-cli-flash +## Upgrading to v6 + +### FlashObject is no longer an EmberObject + +Most apps will be unaffected by this – unless they call `getFlashObject` to access flash messages. + +Prior to v6 `FlashObject` extended [EmberObject](https://api.emberjs.com/ember/release/classes/emberobject/) and supported methods like `get`, `getProperties`, `set`, and `setProperties`. + +It's now a native class, and property access can be done using regular dot syntax, eg `flash.get('message')` should be replaced with `flash.message`. + ## Upgrading to v5 ### Test helpers diff --git a/ember-cli-flash/declarations/flash/object.d.ts b/ember-cli-flash/declarations/flash/object.d.ts index 2a79190e..94168ca3 100644 --- a/ember-cli-flash/declarations/flash/object.d.ts +++ b/ember-cli-flash/declarations/flash/object.d.ts @@ -1,8 +1,6 @@ declare module 'ember-cli-flash/flash/object' { - import EmberObject from '@ember/object'; - import Evented from '@ember/object/evented'; - - class FlashObject extends EmberObject.extend(Evented) { + export default class FlashObject { + message: string; exiting: boolean; exitTimer: number; isExitable: boolean; @@ -14,5 +12,4 @@ declare module 'ember-cli-flash/flash/object' { timerTask(): void; exitTimerTask(): void; } - export default FlashObject; } diff --git a/ember-cli-flash/src/flash/object.js b/ember-cli-flash/src/flash/object.js index 8acbfe8f..86b16dc9 100644 --- a/ember-cli-flash/src/flash/object.js +++ b/ember-cli-flash/src/flash/object.js @@ -1,34 +1,42 @@ -import Evented from '@ember/object/evented'; -import EmberObject, { set } from '@ember/object'; import { cancel, later } from '@ember/runloop'; -import { guidFor } from '../utils/computed'; import { isTesting, macroCondition } from '@embroider/macros'; +import { tracked } from '@glimmer/tracking'; +import { guidFor } from '@ember/object/internals'; +import { destroy, isDestroyed, registerDestructor } from '@ember/destroyable'; // Disable timeout by default when running tests const defaultDisableTimeout = macroCondition(isTesting()) ? true : false; -// Note: -// To avoid https://github.com/adopted-ember-addons/ember-cli-flash/issues/341 from happening, this class can't simply be called Object -export default class FlashObject extends EmberObject.extend(Evented) { +export default class FlashObject { exitTimer = null; - exiting = false; + @tracked exiting = false; + @tracked message = ''; isExitable = true; initializedTime = null; // testHelperDisableTimeout – Set by `disableTimeout` and `enableTimeout` in test-support.js - get disableTimeout() { + get _disableTimeout() { return this.testHelperDisableTimeout ?? defaultDisableTimeout; } - @(guidFor('message').readOnly()) - _guid; + get _guid() { + return guidFor(this.message?.toString()); + } + + constructor(messageOptions) { + for (const [key, value] of Object.entries(messageOptions)) { + this[key] = value; + } - // eslint-disable-next-line ember/classic-decorator-hooks - init() { - super.init(...arguments); + registerDestructor(this, () => { + this.onDestroy?.(); - if (this.disableTimeout || this.sticky) { + this._cancelTimer(); + this._cancelTimer('exitTaskInstance'); + }); + + if (this._disableTimeout || this.sticky) { return; } this.timerTask(); @@ -50,25 +58,14 @@ export default class FlashObject extends EmberObject.extend(Evented) { return; } this.exitTimerTask(); - this.trigger('didExitMessage'); + this.onDidExitMessage?.(); } - - willDestroy() { - if (this.onDestroy) { - this.onDestroy(); - } - - this._cancelTimer(); - this._cancelTimer('exitTaskInstance'); - super.willDestroy(...arguments); - } - preventExit() { - set(this, 'isExitable', false); + this.isExitable = false; } allowExit() { - set(this, 'isExitable', true); + this.isExitable = true; this._checkIfShouldExit(); } @@ -79,19 +76,19 @@ export default class FlashObject extends EmberObject.extend(Evented) { const timerTaskInstance = later(() => { this.exitMessage(); }, this.timeout); - set(this, 'timerTaskInstance', timerTaskInstance); + this.timerTaskInstance = timerTaskInstance; } exitTimerTask() { - if (this.isDestroyed) { + if (isDestroyed(this)) { return; } - set(this, 'exiting', true); + this.exiting = true; if (this.extendedTimeout) { let exitTaskInstance = later(() => { this._teardown(); }, this.extendedTimeout); - set(this, 'exitTaskInstance', exitTaskInstance); + this.exitTaskInstance = exitTaskInstance; } else { this._teardown(); } @@ -99,7 +96,7 @@ export default class FlashObject extends EmberObject.extend(Evented) { _setInitializedTime() { let currentTime = new Date().getTime(); - set(this, 'initializedTime', currentTime); + this.initializedTime = currentTime; return this.initializedTime; } @@ -128,7 +125,7 @@ export default class FlashObject extends EmberObject.extend(Evented) { if (queue) { queue.removeObject(this); } - this.destroy(); - this.trigger('didDestroyMessage'); + destroy(this); + this.onDidDestroyMessage?.(); } } diff --git a/ember-cli-flash/src/services/flash-messages.js b/ember-cli-flash/src/services/flash-messages.js index 40115c3b..174364ec 100644 --- a/ember-cli-flash/src/services/flash-messages.js +++ b/ember-cli-flash/src/services/flash-messages.js @@ -11,6 +11,7 @@ import objectWithout from '../utils/object-without'; import { getOwner } from '@ember/application'; import flashMessageOptions from '../utils/flash-message-options'; import getWithDefault from '../utils/get-with-default'; +import { associateDestroyableChild } from '@ember/destroyable'; export default class FlashMessagesService extends Service { @(equal('queue.length', 0).readOnly()) @@ -103,7 +104,9 @@ export default class FlashMessagesService extends Service { set(flashMessageOptions, key, option); } - return FlashMessage.create(flashMessageOptions); + const message = new FlashMessage(flashMessageOptions); + associateDestroyableChild(this, message); + return message; } _getOptionOrDefault(key, value) { diff --git a/ember-cli-flash/src/utils/computed.js b/ember-cli-flash/src/utils/computed.js deleted file mode 100644 index 0754e6dd..00000000 --- a/ember-cli-flash/src/utils/computed.js +++ /dev/null @@ -1,17 +0,0 @@ -import { computed, get } from '@ember/object'; -import { guidFor as emberGuidFor } from '@ember/object/internals'; -import { isNone } from '@ember/utils'; - -export function guidFor(dependentKey) { - return computed(dependentKey, { - get() { - const value = get(this, dependentKey); - - // it's possible that a value has no toString as some objects don't implement the guid field - // this early return it to avoid errors being thrown when calling undefined.toString() - if (isNone(value)) return; - - return emberGuidFor(value.toString()); - }, - }); -} diff --git a/test-app/tests/integration/components/flash-message-test.js b/test-app/tests/integration/components/flash-message-test.js index 6e5c8a8b..7bf7bd36 100644 --- a/test-app/tests/integration/components/flash-message-test.js +++ b/test-app/tests/integration/components/flash-message-test.js @@ -4,12 +4,14 @@ import { click, find, render, + rerender, settled, triggerEvent, } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import FlashMessage from 'ember-cli-flash/flash/object'; import { next, later } from '@ember/runloop'; +import { isDestroyed } from '@ember/destroyable'; const timeoutDefault = 1000; const TIMEOUT = 50; @@ -18,7 +20,8 @@ module('Integration | Component | flash message', function (hooks) { setupRenderingTest(hooks); test('it renders a flash message', async function (assert) { - this.set('flash', FlashMessage.create({ message: 'hi', sticky: true })); + const flash = new FlashMessage({ message: 'hi', sticky: true }); + this.set('flash', flash); await render(hbs` @@ -26,7 +29,12 @@ module('Integration | Component | flash message', function (hooks) { `); - assert.dom('*').hasText('hi'); + assert.dom('*').hasText('hi', 'initial message is displayed'); + + flash.message = 'hello'; + await rerender(); + + assert.dom('*').hasText('hello', 'updated message is displayed'); }); test('it renders with the right props', async function (assert) { @@ -34,7 +42,7 @@ module('Integration | Component | flash message', function (hooks) { this.set( 'flash', - FlashMessage.create({ + new FlashMessage({ message: 'test', type: 'test', timeout: TIMEOUT, @@ -68,7 +76,7 @@ module('Integration | Component | flash message', function (hooks) { }); test('it does not error when quickly removed from the DOM', async function (assert) { - this.set('flash', FlashMessage.create({ message: 'hi', sticky: true })); + this.set('flash', new FlashMessage({ message: 'hi', sticky: true })); this.set('flag', true); await render(hbs` @@ -82,7 +90,7 @@ module('Integration | Component | flash message', function (hooks) { this.set('flag', false); await settled(); - assert.ok(this.flash.isDestroyed, 'Flash Object isDestroyed'); + assert.ok(isDestroyed(this.flash), 'Flash Object isDestroyed'); }); test('flash message is removed after timeout', async function (assert) { @@ -90,7 +98,7 @@ module('Integration | Component | flash message', function (hooks) { this.set( 'flash', - FlashMessage.create({ + new FlashMessage({ message: 'hi', sticky: false, timeout: timeoutDefault, @@ -108,7 +116,7 @@ module('Integration | Component | flash message', function (hooks) { () => { assert.dom('*').hasText('hi'); assert.notOk( - this.flash.isDestroyed, + isDestroyed(this.flash), 'Flash is not destroyed immediately' ); }, @@ -117,13 +125,13 @@ module('Integration | Component | flash message', function (hooks) { await settled(); - assert.ok(this.flash.isDestroyed, 'Flash Object is destroyed'); + assert.ok(isDestroyed(this.flash), 'Flash Object is destroyed'); }); test('flash message is removed after timeout if mouse enters', async function (assert) { assert.expect(3); - let flashObject = FlashMessage.create({ + let flashObject = new FlashMessage({ message: 'hi', sticky: false, timeout: timeoutDefault, @@ -145,7 +153,7 @@ module('Integration | Component | flash message', function (hooks) { next(this, () => { assert.notOk( - flashObject.isDestroyed, + isDestroyed(flashObject), 'Flash Object is not destroyed' ); triggerEvent('#testFlash', 'mouseleave'); @@ -156,7 +164,7 @@ module('Integration | Component | flash message', function (hooks) { await settled(); - assert.ok(flashObject.isDestroyed, 'Flash Object is destroyed'); + assert.ok(isDestroyed(flashObject), 'Flash Object is destroyed'); }); test('a custom component can use the close closure action', async function (assert) { @@ -164,7 +172,7 @@ module('Integration | Component | flash message', function (hooks) { this.set( 'flash', - FlashMessage.create({ + new FlashMessage({ message: 'flash message content', sticky: true, destroyOnClick: false, @@ -178,21 +186,21 @@ module('Integration | Component | flash message', function (hooks) { `); - assert.notOk(this.flash.isDestroyed, 'flash has not been destroyed yet'); + assert.notOk(isDestroyed(this.flash), 'flash has not been destroyed yet'); await click('.alert'); - assert.notOk(this.flash.isDestroyed, 'flash has not been destroyed yet'); + assert.notOk(isDestroyed(this.flash), 'flash has not been destroyed yet'); await click('.alert a'); assert.ok( - this.flash.isDestroyed, + isDestroyed(this.flash), 'flash is destroyed after clicking close' ); }); test('exiting class is applied for sticky messages', async function (assert) { assert.expect(2); - let flashObject = FlashMessage.create({ + let flashObject = new FlashMessage({ message: 'flash message content', sticky: true, extendedTimeout: 100, @@ -208,12 +216,12 @@ module('Integration | Component | flash message', function (hooks) { await click('.alert'); assert.dom('.alert').hasClass('exiting', 'exiting class is applied'); - assert.ok(flashObject.isDestroyed, 'Flash Object is destroyed'); + assert.ok(isDestroyed(flashObject), 'Flash Object is destroyed'); }); test('custom message type class name prefix is applied', async function (assert) { assert.expect(2); - let flashObject = FlashMessage.create({ + let flashObject = new FlashMessage({ message: 'flash message content', type: 'test', sticky: true, diff --git a/test-app/tests/unit/flash/object-test.js b/test-app/tests/unit/flash/object-test.js index 87d0ff60..b00d3212 100644 --- a/test-app/tests/unit/flash/object-test.js +++ b/test-app/tests/unit/flash/object-test.js @@ -3,6 +3,7 @@ import { isPresent } from '@ember/utils'; import { module, test } from 'qunit'; import FlashMessage from 'ember-cli-flash/flash/object'; import { disableTimeout, enableTimeout } from 'ember-cli-flash/test-support'; +import { isDestroyed } from '@ember/destroyable'; const testTimerDuration = 50; let flash = null; @@ -10,7 +11,7 @@ let flash = null; module('FlashMessageObject disableTimeout', function (hooks) { hooks.beforeEach(function () { disableTimeout(); - flash = FlashMessage.create({ + flash = new FlashMessage({ type: 'test', message: 'Cool story brah', timeout: testTimerDuration, @@ -24,7 +25,7 @@ module('FlashMessageObject disableTimeout', function (hooks) { }); test('it does not create a timer', function (assert) { - assert.notOk(flash.get('timerTaskInstance'), 'it does not create a timer'); + assert.notOk(flash.timerTaskInstance, 'it does not create a timer'); }); }); @@ -32,7 +33,7 @@ module('FlashMessageObject enableTimeout', function (hooks) { hooks.beforeEach(function () { disableTimeout(); enableTimeout(); - flash = FlashMessage.create({ + flash = new FlashMessage({ type: 'test', message: 'Cool story brah', timeout: testTimerDuration, @@ -45,16 +46,13 @@ module('FlashMessageObject enableTimeout', function (hooks) { }); test('it sets a timer after init', function (assert) { - assert.ok( - isPresent(flash.get('timerTaskInstance')), - 'it does create a timer' - ); + assert.ok(isPresent(flash.timerTaskInstance), 'it does create a timer'); }); }); module('FlashMessageObject', function (hooks) { hooks.beforeEach(function () { - flash = FlashMessage.create({ + flash = new FlashMessage({ type: 'test', message: 'Cool story brah', timeout: testTimerDuration, @@ -70,7 +68,7 @@ module('FlashMessageObject', function (hooks) { }); test('it sets a timer after init', function (assert) { - assert.ok(isPresent(flash.get('timerTaskInstance'))); + assert.ok(isPresent(flash.timerTaskInstance)); }); test('it destroys the message after the timer has elapsed', function (assert) { @@ -78,17 +76,17 @@ module('FlashMessageObject', function (hooks) { const done = assert.async(); assert.expect(3); - flash.on('didDestroyMessage', () => { + flash.onDidDestroyMessage = () => { result = 'foo'; - }); + }; later(() => { - assert.true(flash.isDestroyed, 'it sets `isDestroyed` to true'); + assert.true(isDestroyed(flash), 'it sets `isDestroyed` to true'); assert.notOk(flash.timer, 'it cancels the timer'); assert.strictEqual( result, 'foo', - 'it emits the `didDestroyMessage` hook' + 'it called the `onDidDestroyMessage` callback' ); done(); }, testTimerDuration * 2); @@ -98,7 +96,7 @@ module('FlashMessageObject', function (hooks) { const done = assert.async(); assert.expect(1); - const stickyFlash = FlashMessage.create({ + const stickyFlash = new FlashMessage({ type: 'test', message: 'Cool story brah', timeout: testTimerDuration, @@ -107,7 +105,7 @@ module('FlashMessageObject', function (hooks) { }); later(() => { - assert.false(stickyFlash.isDestroyed, 'it is not destroyed'); + assert.false(isDestroyed(stickyFlash), 'it is not destroyed'); done(); }, testTimerDuration); }); @@ -119,22 +117,22 @@ module('FlashMessageObject', function (hooks) { flash.destroyMessage(); }); - assert.true(flash.get('isDestroyed')); - assert.notOk(flash.get('timer')); + assert.true(isDestroyed(flash)); + assert.notOk(flash.timer); }); test('it sets `exiting` to true after the timer has elapsed', function (assert) { assert.expect(2); const done = assert.async(); - const exitFlash = FlashMessage.create({ + const exitFlash = new FlashMessage({ timeout: testTimerDuration, extendedTimeout: testTimerDuration, }); later(() => { - assert.true(exitFlash.get('exiting'), 'it sets `exiting` to true'); - assert.notOk(exitFlash.get('timer'), 'it cancels the `timer`'); + assert.true(exitFlash.exiting, 'it sets `exiting` to true'); + assert.notOk(exitFlash.timer, 'it cancels the `timer`'); done(); }, testTimerDuration * 2); }); @@ -142,7 +140,7 @@ module('FlashMessageObject', function (hooks) { test('it calls `onDestroy` when object is destroyed', function (assert) { assert.expect(1); - const callbackFlash = FlashMessage.create({ + const callbackFlash = new FlashMessage({ sticky: true, onDestroy() { assert.ok(true, 'onDestroy is called'); diff --git a/test-app/tests/unit/utils/computed-test.js b/test-app/tests/unit/utils/computed-test.js deleted file mode 100644 index 8de5307d..00000000 --- a/test-app/tests/unit/utils/computed-test.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable ember/no-classic-classes */ -import { htmlSafe } from '@ember/template'; -import EmberObject from '@ember/object'; -import { guidFor } from 'ember-cli-flash/utils/computed'; -import { module, test } from 'qunit'; - -module('Unit | Utility | computed', function () { - test('#guidFor generates a guid for a `dependentKey`', function (assert) { - const Flash = EmberObject.extend({ - _guid: guidFor('message'), - }); - const flash = Flash.create({ - message: 'I like pie', - }); - const result = flash._guid; - assert.ok(result, 'it generated a guid for `dependentKey`'); - }); - - test('#guidFor generates the same guid for a message', function (assert) { - const Flash = EmberObject.extend({ - _guid: guidFor('message'), - }); - const flash = Flash.create({ - message: htmlSafe('I like pie'), - }); - const secondFlash = Flash.create({ - message: htmlSafe('I like pie'), - }); - const result = flash._guid; - const secondResult = secondFlash._guid; - assert.strictEqual( - result, - secondResult, - 'it generated the same guid for messages that compute to the same string' - ); - }); -});