Click here to Skip to main content
13,250,043 members (68,887 online)
Click here to Skip to main content
Add your own
alternative version

Stats

2.5K views
4 bookmarked
Posted 3 Nov 2017

Programmatically Creating Modals in Ember.js

, 3 Nov 2017
Rate this:
Please Sign up or sign in to vote.
Creating modals (popups) in Ember can open you up to some serious code spaghettification. Here's a quick tip on setting up a re-usable modal creation and control system.

Introduction

"Modals", also referred to as "pop ups", are a very commonly used type of user-interface wherein the user is shown some information and is able to interact with the website, without leaving the page. They're great for showing small-medium amounts of information to a user, whilst keeping the flow of interaction on one page.

This article provides an approach to displaying and controlling the lifecycle of modals within Ember.js applications.

Pre-requisites

Using the Code

The source code is in this repository.

Start off by creating an ember util called ModalUtil.

In terminal:

ember g util modal

This utility will export an extension of the Parse.Object class, including methods set the header and body templates, show and hide the modal, etc.

import Ember from 'ember';

const {computed} = Ember;
export default Ember.Object.extend({

    /**
     * @Property
     * Set on the modal and outlet.
     * The modal element is suffixed with "-modal"
     * by BsModalComponent, but the outlet is not.
     * Therefore, use the computed property 'elementId'
     * to access the DOM element.
     */
    id: null,

    /**
     * @Constructor
     */
    init() {
        this.set('id', Math.random().toString(36).substr(2, 16));
    },

    /**
     * @Property
     * BsModalComponent appends '-modal'
     * to this.id. Use this property to
     * get the modal DOM element.
     **/
    elementId: computed('id', function () {
        return `${this.get('id')}-modal`;
    }),

    /**
     * @Property
     */
    isOpen: false,

    /**
     * @Property
     * Controls the body template partial.
     */
    controller: null,

    /**
     * @Method
     * @param {Ember.Controller} controller
     */
    setController(controller) {
        this.set('controller', controller);
        return this;
    },
    /**
     * @Property
     * Holds model.
     */
    model: null,

    /**
     * @method
     * Sets model on self and if given,
     * the controller too.
     * @param model
     */
    setModel(model) {
        this.set('model', model);
        if (this.get('controller')) {
            this.get('controller').set('model', model);
        }
        return this;
    },

    /**
     * @Property
     */
    title: null,

    /**
     * @method
     * Not required.
     * @param {string} title
     */
    setTitle(title) {
        this.set('title', title);
        return this;
    },
    /**
     * @Property
     * This is the partial for the body.
     */
    bodyTemplate: null,
    /**
     * @method
     * @param {string} template - partial path for the body
     */
    setBodyTemplate(template) {
        this.set('bodyTemplate', template);
        return this;
    },
    /**
     * @Method
     * Sets isOpen to true.
     * @param {Ember.Router|Ember.Route} context - to render the template and controller
     */
    show(context) {
        this.set('isOpen', true);
        context.render(this.get('bodyTemplate'), {
            into: 'application',
            outlet: this.get('id'),
            controller: this.get('controller')
        });

        return this;
    },
    /**
     * @method
     */
    hide() {
        this.set('isOpen', false);
        return this;
    },

    /**
     * @Property
     */
    onClose: null,

    /**
     * @Method
     * @param {function} callback
     */
    setOnCloseCallback(callback) {
        this.set('onClose', callback);
        return this;
    },

    /**
     * @Method
     * @param {function} callback
     */
    setOnSubmitCallback(callback) {
        this.set('onSubmit', callback);
        return this;
    },

    /**
     * @Property
     */
    onHidden: null,
    /**
     * @Property
     */
    onShown: null,
    /**
     * @Property
     */
    onSubmit: null,

    /**
     * @Property
     * If false, use will not be able to close
     * the modal by clicking the back drop.
     */
    backdropClose: true,

    /**
     * @method
     * @param {boolean} [canClose = false]
     */
    disableCloseOnOutsideClick(canClose = false) {
        this.set('backdropClose', canClose);
        return this;
    },
    
    /**
     * @Property
     */
    isWide: false,
    
    /**
     * @method
     * @param {boolean} [isWide = true]
     */
    makeWide(isWide = true) {
        this.set('isWide', isWide);
        return this;
    },

    /**
     * @Property
     * Partial for the header template
     */
    headerTemplate: null,

    /**
     * @method
     * @param {string} partial
     */
    setHeaderTemplate(partial) {
        this.set('headerTemplate', partial);
        return this;
    },

    /**
     * @property
     */
    cancelLabel: 'Cancel',
    submitLabel: 'Submit',
    showCancelButton: false,
    showSubmitButton: false,
    
    /**
     * @method
     * @param {string} label
     */
    setCancelLabel(label) {
        this.set('cancelLabel', label);
        this.displayCancelButton();
        return this;
    },
    /**
     * @method
     * @param {string} label
     */
    setSubmitLabel(label) {
        this.set('submitLabel', label);
        this.displaySubmitButton();
        return this;
    },
    /**
     * @method
     * @param {boolean} [show = true]
     */
    displayCancelButton(show = true) {
        this.set('showCancelButton', show);
        return this;
    },
    /**
     * @method
     * @param {boolean} [show = true]
     */
    displaySubmitButton(show = true) {
        this.set('showSubmitButton', show);
        if (!this.get('onSubmit')) {
            console.error
            ("Modal generated to display submit button, but no 'onSubmit' callback was set.");
        }
        return this;
    },

    /**
     * @Property
     */
    showFooter: computed('showCancelButton', 'showSubmitButton', function () {
        return this.get('showCancelButton')
            || this.get('showSubmitButton');
    })
})

Now, to keep track of active modals across the application, create an ember service called ModalsService.

This service will also abstract the boiler plate aspect of modal creation; setting the body template.

import Ember from 'ember';
import Modal from '../utils/modal';

export default Ember.Service.extend({
    /**
     * @Property
     */
    activeModals: new Ember.A(),

    /**
     * @Property
     * Saves having to get the DOM element
     * each time. Its needed to ensure
     * the 'modal-open' class is set on
     * the body.
     */
    bodyElement: null,

    /**
     * @method
     * @param {string} bodyTemplate - partial for the main body template
     * @param {object} [options] - title
     */
    buildModal(bodyTemplate, options = {}) {
        let modal = Modal.create();

        if (!this.get('bodyElement')) {
            Ember.run.later(() => {
                this.set('bodyElement', Ember.$("body"));
            }, 100);
        }

        modal.setBodyTemplate(bodyTemplate);
        modal.set('onHidden', (function () {
            // First fire onClose callback if provided
            // modal controller is required.
            if (modal.get('onClose')) {
                if (!modal.get('controller')) {
                    throw new Error("Cannot build modal: given onClose but no controller provided.");
                }
                modal.get('onClose')(modal.get('controller'));
            }
            // Then fire the removeModal method.
            this.removeModal(modal.get('id'));

            // Check if another modal has been opened whilst this one was close
            // If so, ensure that the body element has class 'modal-open'
            if (this.get('activeModals.length') && 
            !this.get('bodyElement').hasClass('modal-open')) {
                Ember.run.later(() => {
                    this.get('bodyElement').addClass('modal-open')
                }, 100);
            }

        }.bind(this)));

        if (options.disableOutsideClick) {
            modal.disableCloseOnOutsideClick()
        }

        if (options.wide) {
            modal.makeWide();
        }

        if (options.onClose) {
            modal.setOnCloseCallback(options.onClose);
        }
        if (options.onSubmit) {
            modal.setOnSubmitCallback(options.onSubmit);
        }

        if (options.title) {
            modal.setTitle(options.title);
        }

        if (options.showSubmitButton) {
            modal.displaySubmitButton();
        }
        if (options.showCancelButton) {
            modal.displayCancelButton();
        }
        if (options.submitLabel) {
            modal.setSubmitLabel(options.submitLabel);
        }
        if (options.cancelLabel) {
            modal.setCancelLabel(options.cancelLabel);
        }

        this.get('activeModals').pushObject(modal);

        return modal;
    },

    /**
     * @method
     * Finds, removes and destroys
     * modal.
     * @param {String} modalId
     */
    removeModal(modalId) {
        if (!modalId) {
            return;
        }
        let modal = this.get('activeModals').findBy('id', modalId);
        if (modal) {
            this.get('activeModals').removeObject(modal);
            modal.destroy();
        }
        return this;
    },

    /**
     * @Method
     * @param {Modal} modal
     * @param {Ember.Controller} context
     */
    showOnly(modal, context) {
        modal.show(context);
        Ember.run.later(() => {
            this.closeAll(modal);
        }, 100);
        return this;
    },

    /**
     * @Method
     * Closes and destroys all modals.
     * @param {Modal} [exceptThisModal]
     */
    closeAll(exceptThisModal) {
        this.get('activeModals').forEach((modal) => {
            if (!exceptThisModal || modal.get('id') !== exceptThisModal.get('id')) {
                modal.hide();
            }
        });
        return this;
    }
});

The array activeModals is maintained by this service and can be accessed (observed or computed) by injecting routes. Before using the build modal method, let's set the activeModal structure template in the ApplicationTemplate (application.hbs).

{{!-- The following component displays Ember's default welcome message. --}}
{{welcome-page}}
{{!-- Feel free to remove this! --}}

{{outlet}}

{{#each activeModals as |activeModal|}}
    {{#bs-modal
        id=activeModal.id
        open=activeModal.isOpen
        onHidden=activeModal.onHidden
        onSubmit=activeModal.onSubmit
        footer=false
        backdropClose=activeModal.backdropClose
        class=(if activeModal.isWide 'modal-wide')
as |modal|
}}
    {{#if activeModal.title}}
        {{#modal.header}}
            {{#if activeModal.headerTemplate}}
                {{partial activeModal.headerTemplate}}
            {{else}}
                <h4 class="modal-title">{{activeModal.title}}</h4>
            {{/if}}
        {{/modal.header}}
    {{/if}}
    {{#modal.body}}
       
            {{outlet activeModal.id}}
       
    {{/modal.body}}
    {{#if activeModal.showFooter}}
        {{#modal.footer as |footer|}}
            {{#if activeModal.showCancelButton}}
                {{#bs-button onClick=(action modal.close) 
                type="default"}}{{activeModal.cancelLabel}}{{/bs-button}}
            {{/if}}
            {{#if activeModal.showSubmitButton}}
                {{#bs-button onClick=(action modal.submit) 
                type=activeModal.submitButtonType}}{{activeModal.submitLabel}}{{/bs-button}}
            {{/if}}
        {{/modal.footer}}
    {{/if}}
{{/bs-modal}}
{{/each}}

Now the good part, create a simple body template and an action to use the methods above to build and show the modal. I tend to create the body templates in a subfolder for the route that creates the modal, in this case, 'application/modal/login':

ember g template application/modal/login
  • - application
  • -- modal
  • --- login.hbs
<label>Email</label>
{{input type='email'
        value=email}}<span id="cke_bm_141E" style="display: none;"> </span>

<label>Password</label>
{{input type='password'
        value=password}}

<button class='btn btn-success'>Login</button>

Within ApplicationRoute, set an action that builds and displays this modal. First, ensure you have an ApplicationRoute file; if not, run the following command and press 'N' if it asks you to overwrite the template.

ember g route application

Inject ModalsService before creating the action.

import Ember from 'ember';
const {inject: {serivce}} = Ember;
export default Ember.Route.extend({
    
    modals: service(),
    
    actions: {        
        openLoginModal() {
            let loginModal = this.get('modals')
                .buildModal('application/modal/login')
                .setTitle('Login')
                .setController(this.controllerFor('application'))
                .show(this);            
        },
        
        closeAllModals() {
            this.get('modals').closeAll();
        }        
    }    
});

And that's it! Simply trigger the openLoginModal action and you'll have the modal up! You can inject ModalService into any route.

Points of Interest

Careful when setting the controller and model on the modal; if you are using the controller for another route or template, setting a different model will inactivate that route/template and therefore cause unexpected behaviour.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Omair Vaiyani
Chief Technology Officer Synap
United Kingdom United Kingdom
I'm a full-stack developer with a particular focus on Ember.js, Node.js and Android. I founded an educational technology company whilst at medical school, and have now left the profession after graduation to work full-time on my company, Synap

Synap uses machine learning to tailor make study plans for its students.

You may also be interested in...

Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 3 Nov 2017
Article Copyright 2017 by Omair Vaiyani
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid