I. Getting started
The below will be used in our demo:
- Ember 2.6.1
- JQuery 2.1.4
- Emblem 0.9 (This is a template for ember. You can use Handlebars instead of this).
- ember-route-action-helper 2.0.0
- ember-wormhole 0.4.1 (There is an issue about displaying modal-backdrop so I have used it to resolve).
II. Let’s start
1. Add modal outlet to our application template
#ember-modal-container
= outlet 'modal'
- The
#ember-modal-container
div is whereember-wormhole
will move block code inside. outlet 'modal'
is the place where the template of modal component will be rendered.
2. Add openModal
and closeModal
actions to application route
const MODAL_DEFAULT_DESTINATION = 'ember-modal-container';
const MODAL_DEFAULT_OUTLET = 'modal';
const { run, on } = Ember;
export default Ember.Route.extend({
currentLayout: 'application',
actions: {
openModal(modalName, model, modalDefaultDestination = MODAL_DEFAULT_DESTINATION) {
this.render(`modals/${modalName}`, {
model,
outlet: MODAL_DEFAULT_OUTLET,
into: this.get('currentLayout'),
emberWormholeDestination: modalDefaultDestination,
});
},
closeModal() {
this.disconnectOutlet({
outlet: MODAL_DEFAULT_OUTLET,
parentView: this.get('currentLayout')
});
}
}
});
- openModal:
modalName
: name of controller that will be rendered into the modal.model
: the model of the controllermodalDefaultDestination
: whereember-wormhole
will move the block code inside. Here, it is theember-modal-container
div.
3. Add template/component for modal
Here are my 2 components for the modal:
3.1 bootstrap/bs-modal
- Component
const { computed, observer } = Ember;
const Modal = {};
Modal.TRANSITION_DURATION = 300;
Modal.BACKDROP_TRANSITION_DURATION = 150;
const observeOpen = function() {
if (this.get('open')) {
this.show();
} else {
this.hide();
}
};
export default Ember.Component.extend({
emberWormholeDestination: 'ember-modal-container',
extraClass: null,
open: true,
title: null,
closeButton: true,
fade: true,
'in': false,
backdrop: true,
showBackdrop: false,
keyboard: true,
autoClose: true,
modalId: computed('elementId', function() {
return `${this.get('elementId')}-modal`;
}),
modalElement: computed('modalId', function() {
return Ember.$(`#${this.get('modalId')}`);
}).volatile(),
backdropId: computed('elementId', function() {
return `${this.get('elementId')}-backdrop`;
}),
backdropElement: computed('backdropId', function() {
return Ember.$(`#${this.get('backdropId')}`);
}).volatile(),
usesTransition: computed('fade', function() {
return Ember.$.support.transition && this.get('fade');
}),
size: null,
backdropClose: true,
renderInPlace: false,
submitAction: null,
closeAction: null,
closedAction: null,
openAction: null,
openedAction: null,
actions: {
close() {
if (this.get('autoClose')) {
this.set('open', false);
}
this.sendAction('closeAction');
},
submit() {
let form = this.get('modalElement').find('.modal-body form');
if (form.length > 0) {
// trigger submit event on body form
form.trigger('submit');
} else {
// if we have no form, we send a submit action
this.sendAction('submitAction');
}
}
},
_observeOpen: observer('open', observeOpen),
takeFocus() {
let focusElement = this.get('modalElement').find('[autofocus]').first();
if (focusElement.length === 0) {
focusElement = this.get('modalElement');
}
if (focusElement.length > 0) {
focusElement.focus();
}
},
show() {
this.checkScrollbar();
this.setScrollbar();
Ember.$('body').addClass('modal-open');
this.resize();
let callback = function() {
if (this.get('isDestroyed')) {
return;
}
this.get('modalElement')
.show()
.scrollTop(0);
this.handleUpdate();
this.set('in', true);
this.sendAction('openAction');
if (this.get('usesTransition')) {
this.get('modalElement')
.one('bsTransitionEnd', Ember.run.bind(this, function() {
this.takeFocus();
this.sendAction('openedAction');
}))
.emulateTransitionEnd(Modal.TRANSITION_DURATION);
} else {
this.takeFocus();
this.sendAction('openedAction');
}
};
Ember.run.scheduleOnce('afterRender', this, this.handleBackdrop, callback);
},
hide() {
this.resize();
this.set('in', false);
if (this.get('usesTransition')) {
this.get('modalElement')
.one('bsTransitionEnd', Ember.run.bind(this, this.hideModal))
.emulateTransitionEnd(Modal.TRANSITION_DURATION);
} else {
this.hideModal();
}
},
hideModal() {
if (this.get('isDestroyed')) {
return;
}
this.get('modalElement').hide();
this.handleBackdrop(() => {
Ember.$('body').removeClass('modal-open');
this.resetAdjustments();
this.resetScrollbar();
this.sendAction('closedAction');
});
},
handleBackdrop(callback) {
let doAnimate = this.get('usesTransition');
if (this.get('open') && this.get('backdrop')) {
this.set('showBackdrop', true);
if (!callback) {
return;
}
let waitForFade = function() {
let $backdrop = this.get('backdropElement');
Ember.assert('Backdrop element should be in DOM', $backdrop && $backdrop.length > 0);
if (doAnimate) {
$backdrop
.one('bsTransitionEnd', Ember.run.bind(this, callback))
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION);
} else {
callback.call(this);
}
};
Ember.run.scheduleOnce('afterRender', this, waitForFade);
} else if (!this.get('open') && this.get('backdrop')) {
let $backdrop = this.get('backdropElement');
Ember.assert('Backdrop element should be in DOM', $backdrop && $backdrop.length > 0);
let callbackRemove = function() {
this.set('showBackdrop', false);
if (callback) {
callback.call(this);
}
};
if (doAnimate) {
$backdrop
.one('bsTransitionEnd', Ember.run.bind(this, callbackRemove))
.emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION);
} else {
callbackRemove.call(this);
}
} else if (callback) {
callback.call(this);
}
},
resize() {
if (this.get('open')) {
Ember.$(window).on('resize.bs.modal', Ember.run.bind(this, this.handleUpdate));
} else {
Ember.$(window).off('resize.bs.modal');
}
},
handleUpdate() {
this.adjustDialog();
},
adjustDialog() {
let modalIsOverflowing = this.get('modalElement')[0].scrollHeight > document.documentElement.clientHeight;
this.get('modalElement').css({
paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.get('scrollbarWidth') : '',
paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.get('scrollbarWidth') : ''
});
},
resetAdjustments() {
this.get('modalElement').css({
paddingLeft: '',
paddingRight: ''
});
},
checkScrollbar() {
let fullWindowWidth = window.innerWidth;
if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
let documentElementRect = document.documentElement.getBoundingClientRect();
fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left);
}
this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth;
},
setScrollbar() {
let bodyPad = parseInt((Ember.$('body').css('padding-right') || 0), 10);
this.originalBodyPad = document.body.style.paddingRight || '';
if (this.bodyIsOverflowing) {
Ember.$('body').css('padding-right', bodyPad + this.get('scrollbarWidth'));
}
},
resetScrollbar() {
Ember.$('body').css('padding-right', this.originalBodyPad);
},
scrollbarWidth: computed(function() {
let scrollDiv = document.createElement('div');
scrollDiv.className = 'modal-scrollbar-measure';
this.get('modalElement').after(scrollDiv);
let scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
Ember.$(scrollDiv).remove();
return scrollbarWidth;
}),
didInsertElement() {
if (this.get('open')) {
this.show();
}
},
willDestroyElement() {
Ember.$(window).off('resize.bs.modal');
Ember.$('body').removeClass('modal-open');
}
});
- Template
= ember-wormhole to=emberWormholeDestination renderInPlace=renderInPlace
= bootstrap/bs-modal-dialog close=(action "close") fade=fade in=in id=modalId title=title closeButton=closeButton keyboard=keyboard size=size extraClass=extraClass backdropClose=backdropClose
yield this
if showBackdrop
.modal-backdrop class="modal-backdrop {{if fade "fade"}} {{if in "in"}}" id="{{backdropId}}"
3.2 bootstrap/bs-modal-dialog
- Component
import Ember from 'ember';
const { computed } = Ember;
export default Ember.Component.extend({
classNames: ['modal', 'modal-center'],
classNameBindings: ['fade', 'in'],
attributeBindings: ['tabindex'],
ariaRole: 'dialog',
tabindex: '-1',
title: null,
closeButton: true,
fade: true,
'in': false,
keyboard: true,
size: null,
backdropClose: true,
sizeClass: computed('size', function() {
let size = this.get('size');
return Ember.isBlank(size) ? null : `modal-${size}`;
}),
keyDown(e) {
let code = e.keyCode || e.which;
if (code === 27 && this.get('keyboard')) {
this.sendAction('close');
}
},
click(e) {
if (e.target !== e.currentTarget || !this.get('backdropClose')) {
return;
}
this.sendAction('close');
}
});
- Template
.modal-dialog class="{{sizeClass}} {{extraClass}}"
.modal-content
if title
h4.modal-header.text-xs-center
= title
= yield
III. How to use
Example:
i.fa.fa-share-square-o.card-share click="action 'openModal' 'quote/sharing' quote"
quote/sharing
: the controller you want to display in the modal.quote
: the model for this controller.
IV. More :D
If you want to modify the URL without reloading the page when the modal is opened, similar to Facebook, you can use the following code:
const MODAL_DEFAULT_DESTINATION = 'ember-modal-container';
const MODAL_DEFAULT_OUTLET = 'modal';
const { run, on } = Ember;
export default Ember.Route.extend({
currentLayout: 'application',
beforeModalUrl: null,
actions: {
openModal(modalName, model, replaceCurrentUrl = true, modalDefaultDestination = MODAL_DEFAULT_DESTINATION) {
Ember.run.next(() => {
this.set('beforeModalUrl', this.get('router.url'));
if (replaceCurrentUrl) {
let url = null;
let modalRouteName = `${model.constructor.modelName}.index`;
let urlOpts = { display: 'popup' };
url = `${this.router.generate(modalRouteName, model)}?${Ember.$.param(urlOpts)}`;
if (url) {
window.history.replaceState({}, modalRouteName, url);
}
}
this.render(`modals/${modalName}`, {
model,
outlet: MODAL_DEFAULT_OUTLET,
into: this.get('currentLayout'),
emberWormholeDestination: modalDefaultDestination,
});
});
},
closeModal() {
this.disconnectOutlet({
outlet: MODAL_DEFAULT_OUTLET,
parentView: this.get('currentLayout')
});
if (this.get('beforeModalUrl')) {
Ember.run(() => {
window.location.href = this.router.location.formatURL(this.get('beforeModalUrl'));
this.set('beforeModalUrl', null);
});
}
}
}
});
replaceCurrentUrl
: enable/disable modifying the URLbeforeModalUrl
: store previous URL
Public comments are closed, but I love hearing from readers. Feel free to contact me with your thoughts.