Skip to content

Commit

Permalink
fix: resolve modal with function
Browse files Browse the repository at this point in the history
  • Loading branch information
julianpereznext authored and josex2r committed Dec 21, 2021
1 parent ad8ed8e commit a1d135f
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 650 deletions.
76 changes: 36 additions & 40 deletions addon/components/modal.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import Component from '@ember/component';
import { camelize } from '@ember/string';
import { computed } from '@ember/object';
import { computed, action } from '@ember/object';
import onTransitionEnd from 'ember-transition-end/utils/on-transition-end';
import { hasTransitions } from 'ember-modal-service/utils/css-transitions';
import { inject as service } from '@ember/service';
import { run } from '@ember/runloop';
import { next } from '@ember/runloop';
import { tracked } from '@glimmer/tracking';
import { buildWaiter } from '@ember/test-waiters';

export default class ModalComponent extends Component.extend({
// Needed to be able to reopen `resolve` and `reject` methods.
actions: {
resolve() {
this.resolve(...arguments);
},
reject() {
this.reject(...arguments);
}
}
}) {
@service scheduler;
const openWaiter = buildWaiter('ember-modal-service:open-waiter');
const closeWaiter = buildWaiter('ember-modal-service:close-waiter');

export default class ModalComponent extends Component {
@service modal;

attributeBindings = ['data-modal-show', 'data-id'];
Expand All @@ -41,21 +33,9 @@ export default class ModalComponent extends Component.extend({
init() {
super.init(...arguments);

// Prevent creating an uncaught promise.
this.model.promise.catch(() => {}).finally(
this._close.bind(this),
`Component '${this.model.fullname}': close modal`
);
}

didInsertElement() {
super.didInsertElement(...arguments);

run.next(this.scheduler, 'scheduleOnce', this, '_open');
next(this, '_open');
}

didOpen() {}

_safeDidOpen() {
if (this.isDestroyed) {
return;
Expand All @@ -70,40 +50,50 @@ export default class ModalComponent extends Component.extend({
return;
}

const scheduler = this.scheduler;
const element = this.element;

this.visible = true;

if (hasTransitions(element)) {
onTransitionEnd(element, scheduler.scheduleOnce.bind(scheduler, this, '_safeDidOpen'), {
const token = openWaiter.beginAsync();
const callback = () => {
openWaiter.endAsync(token);
this._safeDidOpen();
};

onTransitionEnd(this.element, callback, {
transitionProperty: 'all',
once: true,
onlyTarget: true
onlyTarget: true,
});
} else {
this.didOpen();
this._safeDidOpen();
}
}

_close() {
// istanbul ignore if: lifecycle check.
if (this.isDestroyed) {
if (this.isDestroyed || this.isDestroying) {
return;
}

const scheduler = this.scheduler;
const element = this.element;

// Close modal.
this.visible = false;

// Remove modal from array when transition ends.
if (hasTransitions(element)) {
onTransitionEnd(element, scheduler.scheduleOnce.bind(scheduler, this, '_remove'), {
const token = closeWaiter.beginAsync();
const callback = () => {
closeWaiter.endAsync(token);
this._remove();
};

onTransitionEnd(this.element, callback, {
transitionProperty: 'all',
once: true,
onlyTarget: true
onlyTarget: true,
});
} else {
this._remove();
Expand All @@ -119,17 +109,23 @@ export default class ModalComponent extends Component.extend({
this.modal._closeByModel(this.model);
}

resolve(data, label = `Component '${this.model.fullname}': fulfillment`) {
this.model.resolve(data, label);
@action
resolve(data) {
this._fullfillmentFn = () => this.model.resolve(data);

this._close();
}

reject(data, label = `Component '${this.model.fullname}': rejection`) {
this.model.reject(data, label);
@action
reject(error) {
this._fullfillmentFn = () => this.model.reject(error);

this._close();
}

willDestroy() {
super.willDestroy(...arguments);

this.modal.trigger('will-destroy', this.model);
this._fullfillmentFn && this._fullfillmentFn();
}
}
16 changes: 7 additions & 9 deletions addon/services/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,16 @@ export default class ModalService extends Service.extend(Evented) {

this.trigger('open', model);

return new Promise((resolve) => {
this.one('close', () => resolve(model.promise.catch(() => {})));
});
model.promise
.catch(() => {})
.finally(() => {
this.trigger('close', model);
});

return model.promise;
}

_closeByModel(model) {
const destroyCallback = (destroyedModal) => {
destroyedModal === model && this.trigger('close', model);
};

// Setup DOM removal listener
this.one('will-destroy', destroyCallback);
// Remove from DOM
this.content.removeObject(model);
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"@glimmer/tracking": "^1.0.1",
"ember-cli-babel": "^7.19.0",
"ember-cli-htmlbars": "^5.3.1",
"ember-task-scheduler": "^2.1.0",
"ember-transition-end": "^2.0.0"
"ember-transition-end": "^2.0.0",
"@ember/test-waiters": "^3.0.0"
},
"devDependencies": {
"@commitlint/cli": "^9.1.2",
Expand Down
178 changes: 178 additions & 0 deletions tests/acceptance/modal-component-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { module, test } from 'qunit';
import { click, render, settled, waitFor } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import cases from 'qunit-parameterize';

module('Acceptance | modal-component', (hooks) => {
setupRenderingTest(hooks);

hooks.beforeEach(async function(assert) {
assert.timeout(5000);

const modal = this.owner.lookup('service:modal');

this.open = () => modal.open('custom-modal');
this.waitForRender = () => waitFor('[data-id="modalCustomModal"]');
this.waitForVisible = async() => {
await settled();
await waitFor(
'[data-id="modalCustomModal"][data-modal-show="true"]'
);
};

await render(hbs`<ModalContainer />`);
});

test('it defines the appropriate `data-id` on the component wrapper', async function(assert) {
this.open();

await this.waitForRender();

assert.dom('[data-id="modalCustomModal"]').exists();

await settled();
});

test('it is accessible', async function(assert) {
this.open();

await this.waitForRender();

assert
.dom('[data-id="modalCustomModal"]')
.hasAttribute('role', 'dialog');

// Resolve modal to remove pending waiters
await click('[data-id="resolve"]');
});

test('it renders hidden and then toggles visibility', async function(assert) {
this.open();

await this.waitForRender();

assert
.dom('[data-id="modalCustomModal"]')
.hasAttribute('data-modal-show', 'false');

await this.waitForVisible();

assert
.dom('[data-id="modalCustomModal"]')
.hasAttribute('data-modal-show', 'true');
});

cases([{ title: 'resolve' }, { title: 'reject' }]).test(
'it changes visibility when modal is closing ',
async function({ title: method }, assert) {
this.open();

await this.waitForVisible();
click(`[data-id="${method}"]`);
await waitFor(
'[data-id="modalCustomModal"][data-modal-show="false"]'
);

assert
.dom('[data-id="modalCustomModal"]')
.hasAttribute('data-modal-show', 'false');

await settled();
}
);

cases([{ title: 'resolve' }, { title: 'reject' }]).test(
'it removes modal from DOM when promise is fulfilled ',
async function({ title: method }, assert) {
const promise = this.open();

await this.waitForVisible();
click(`[data-id="${method}"]`);

try {
await promise;
} catch {
// Nope...
}

assert.dom('[data-id="modalCustomModal"]').doesNotExist();
}
);

cases([{ title: 'resolve' }, { title: 'reject' }]).test(
'it fulfills with a value ',
async function({ title: method }, assert) {
const promise = this.open();

await this.waitForVisible();
click(`[data-id="${method}"]`);

try {
const value = await promise;

assert.equal(typeof value, 'function');
} catch (e) {
assert.equal(e, 'reject');
}
}
);

test('it calls "didOpen" when modal is visible', async function(assert) {
const promise = this.open();

await this.waitForVisible();
await new Promise((res) => setTimeout(res, 500)); // wait transition callback
click('[data-id="resolve"]');

const didOpenSpy = await promise;

assert.ok(didOpenSpy.calledOnce);
});

module('animations disabled', (moduleHooks) => {
moduleHooks.beforeEach(function() {
this.styles = document.createElement('style');
this.styles.innerHTML = `
[data-id="modalCustomModal"] {
transition: none !important;
}
`;
document.body.appendChild(this.styles);
});

moduleHooks.afterEach(function() {
document.body.removeChild(this.styles);
});

test('it calls "didOpen" when modal is visible', async function(assert) {
const promise = this.open();

await this.waitForVisible();
await new Promise((res) => setTimeout(res, 500)); // wait transition callback
click('[data-id="resolve"]');

const didOpenSpy = await promise;

assert.ok(didOpenSpy.calledOnce);
});

cases([{ title: 'resolve' }, { title: 'reject' }]).test(
'it removes modal from DOM when promise is fulfilled ',
async function({ title: method }, assert) {
const promise = this.open();

await this.waitForVisible();
click(`[data-id="${method}"]`);

try {
await promise;
} catch {
// Nope...
}

assert.dom('[data-id="modalCustomModal"]').doesNotExist();
}
);
});
});
4 changes: 2 additions & 2 deletions tests/dummy/app/components/modal-custom-modal/index.hbs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<button type="button" data-id="resolve" {{on 'click' (action this.resolve this 'foo')}}>Resolve</button>
<button type="button" data-id="reject" {{on 'click' (action this.reject 'reject')}}>Reject</button>
<button type="button" data-id="resolve" {{on 'click' (fn this.resolve this.didOpen)}}>Resolve</button>
<button type="button" data-id="reject" {{on 'click' (fn this.reject 'reject')}}>Reject</button>
5 changes: 4 additions & 1 deletion tests/dummy/app/components/modal-custom-modal/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import ModalComponent from 'ember-modal-service/components/modal';
import sinon from 'sinon';

export default class CustomModalComponent extends ModalComponent {}
export default class CustomModalComponent extends ModalComponent {
didOpen = sinon.spy();
}
18 changes: 9 additions & 9 deletions tests/dummy/app/styles/app.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.animated {
transition: opacity .2s
}

.animated[data-modal-show="false"] {
[data-id='modalCustomModal'] {
transition: opacity 0.2s;
}
[data-id='modalCustomModal'][data-modal-show='false'] {
opacity: 0;
}

.animated[data-modal-show="true"] {
}
[data-id='modalCustomModal'][data-modal-show='true'] {
opacity: 1;
}
}

0 comments on commit a1d135f

Please sign in to comment.