Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert FlashObject to native class #394

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Upgrading ember-cli-flash

## Upgrading to v6
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming next release will be v6, although minor, this is a breaking change.

Would be good to batch all the remaining internal changes and TypeScript migration in the same update...


### 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
Expand Down
7 changes: 2 additions & 5 deletions ember-cli-flash/declarations/flash/object.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,5 +12,4 @@ declare module 'ember-cli-flash/flash/object' {
timerTask(): void;
exitTimerTask(): void;
}
export default FlashObject;
}
67 changes: 32 additions & 35 deletions ember-cli-flash/src/flash/object.js
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +12 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This this is no longer an EmberObject. Only the tracked properties will update rendered flash messages. We may want to track type as well, although I'm not sure if that would even work.

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();
Expand All @@ -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();
}

Expand All @@ -79,27 +76,27 @@ 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();
}
}

_setInitializedTime() {
let currentTime = new Date().getTime();
set(this, 'initializedTime', currentTime);
this.initializedTime = currentTime;

return this.initializedTime;
}
Expand Down Expand Up @@ -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?.();
}
}
5 changes: 4 additions & 1 deletion ember-cli-flash/src/services/flash-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 0 additions & 17 deletions ember-cli-flash/src/utils/computed.js

This file was deleted.

44 changes: 26 additions & 18 deletions test-app/tests/integration/components/flash-message-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,23 +20,29 @@ 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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the FlashMessage need associateDestroyableChild when it's created?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea. I've implemented this in FlashMessagesService when the messages are created

this.set('flash', flash);

await render(hbs`
<FlashMessage @flash={{this.flash}} as |component flash|>
{{flash.message}}
</FlashMessage>
`);

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) {
assert.expect(3);

this.set(
'flash',
FlashMessage.create({
new FlashMessage({
message: 'test',
type: 'test',
timeout: TIMEOUT,
Expand Down Expand Up @@ -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`
Expand All @@ -82,15 +90,15 @@ 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) {
assert.expect(3);

this.set(
'flash',
FlashMessage.create({
new FlashMessage({
message: 'hi',
sticky: false,
timeout: timeoutDefault,
Expand All @@ -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'
);
},
Expand All @@ -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,
Expand All @@ -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');
Expand All @@ -156,15 +164,15 @@ 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) {
assert.expect(3);

this.set(
'flash',
FlashMessage.create({
new FlashMessage({
message: 'flash message content',
sticky: true,
destroyOnClick: false,
Expand All @@ -178,21 +186,21 @@ module('Integration | Component | flash message', function (hooks) {
</FlashMessage>
`);

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,
Expand All @@ -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,
Expand Down
Loading
Loading