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 6 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
8 changes: 3 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 @@ -13,6 +11,6 @@ declare module 'ember-cli-flash/flash/object' {
allowExit(): void;
timerTask(): void;
exitTimerTask(): void;
destroy(): void;
}
export default FlashObject;
}
49 changes: 25 additions & 24 deletions ember-cli-flash/src/flash/object.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
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';

// 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;
isDestroyed = false;

// 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());
}

// eslint-disable-next-line ember/classic-decorator-hooks
init() {
super.init(...arguments);
constructor(messageOptions) {
for (const [key, value] of Object.entries(messageOptions)) {
this[key] = value;
}

if (this.disableTimeout || this.sticky) {
if (this._disableTimeout || this.sticky) {
return;
}
this.timerTask();
Expand All @@ -50,25 +51,25 @@ export default class FlashObject extends EmberObject.extend(Evented) {
return;
}
this.exitTimerTask();
this.trigger('didExitMessage');
this.onDidExitMessage?.();
}

willDestroy() {
destroy() {
if (this.onDestroy) {
this.onDestroy();
}

this._cancelTimer();
this._cancelTimer('exitTaskInstance');
super.willDestroy(...arguments);
this.isDestroyed = true;
}

preventExit() {
set(this, 'isExitable', false);
this.isExitable = false;
}

allowExit() {
set(this, 'isExitable', true);
this.isExitable = true;
this._checkIfShouldExit();
}

Expand All @@ -79,27 +80,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) {
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 @@ -129,6 +130,6 @@ export default class FlashObject extends EmberObject.extend(Evented) {
queue.removeObject(this);
}
this.destroy();
this.trigger('didDestroyMessage');
this.onDidDestroyMessage?.();
}
}
2 changes: 1 addition & 1 deletion ember-cli-flash/src/services/flash-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default class FlashMessagesService extends Service {
set(flashMessageOptions, key, option);
}

return FlashMessage.create(flashMessageOptions);
return new FlashMessage(flashMessageOptions);
}

_getOptionOrDefault(key, value) {
Expand Down
17 changes: 0 additions & 17 deletions ember-cli-flash/src/utils/computed.js

This file was deleted.

25 changes: 16 additions & 9 deletions test-app/tests/integration/components/flash-message-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
click,
find,
render,
rerender,
settled,
triggerEvent,
} from '@ember/test-helpers';
Expand All @@ -18,23 +19,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 +75,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 @@ -90,7 +97,7 @@ module('Integration | Component | flash message', function (hooks) {

this.set(
'flash',
FlashMessage.create({
new FlashMessage({
message: 'hi',
sticky: false,
timeout: timeoutDefault,
Expand Down Expand Up @@ -123,7 +130,7 @@ module('Integration | Component | flash message', function (hooks) {
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 Down Expand Up @@ -164,7 +171,7 @@ module('Integration | Component | flash message', function (hooks) {

this.set(
'flash',
FlashMessage.create({
new FlashMessage({
message: 'flash message content',
sticky: true,
destroyOnClick: false,
Expand Down Expand Up @@ -192,7 +199,7 @@ module('Integration | Component | flash message', function (hooks) {

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 @@ -213,7 +220,7 @@ module('Integration | Component | flash message', function (hooks) {

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
35 changes: 16 additions & 19 deletions test-app/tests/unit/flash/object-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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,
Expand All @@ -24,15 +24,15 @@ 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');
});
});

module('FlashMessageObject enableTimeout', function (hooks) {
hooks.beforeEach(function () {
disableTimeout();
enableTimeout();
flash = FlashMessage.create({
flash = new FlashMessage({
type: 'test',
message: 'Cool story brah',
timeout: testTimerDuration,
Expand All @@ -45,16 +45,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,
Expand All @@ -70,25 +67,25 @@ 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) {
let result;
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.notOk(flash.timer, 'it cancels the timer');
assert.strictEqual(
result,
'foo',
'it emits the `didDestroyMessage` hook'
'it called the `onDidDestroyMessage` callback'
);
done();
}, testTimerDuration * 2);
Expand All @@ -98,7 +95,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,
Expand All @@ -119,30 +116,30 @@ module('FlashMessageObject', function (hooks) {
flash.destroyMessage();
});

assert.true(flash.get('isDestroyed'));
assert.notOk(flash.get('timer'));
assert.true(flash.isDestroyed);
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);
});

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');
Expand Down
Loading
Loading