wheely/ember-dialog

View on GitHub
addon/services/dialog.js

Summary

Maintainability
B
4 hrs
Test Coverage
import Ember from 'ember';
import ContextMixin from 'ember-dialog/mixins/context';
import Configuration from 'ember-dialog/configuration';

const { guidFor, getOwner} = Ember;

const DEFAULT_COMPONENT_NAME = "presenter";

/**
 * The built in string object.
 * @external Ember/RSVP/Promise
 * @see {@link http://emberjs.com/api/classes/RSVP.Promise.html}
 */

/**
 * @module ember-dialog/services/dialog
 * @augments Ember.Service
 * @augments Ember.Evented
 * @uses module:ember-dialog/mixins/context
 */
export default Ember.Service.extend(Ember.Evented, {

  /**
   * The DOM selector that used to append dialogs to.
   * @type {String}
   */
  rootElement: 'body',

  /**
   * @type {Array}
   * @protected
   */
  dialogs: Ember.A(),

  /**
   * @property {String} className   CSS class name
   * @protected
   */
  defaults: {
    className: ''
  },

  init() {
    this.on("created", presenter => this.created(presenter));
    this.on("destroyed", presenter => this.destroyed(presenter));
    return this._super(...arguments);
  },

  /**
   * @method
   * @param {module:ember-dialog/components/presenter} presenter
   */
  add(presenter) {
    var id = presenter.get("presenterId") || guidFor(presenter);
    this.set("dialogs", Ember.A(this.get("dialogs").filter(item => item.id !== id)));
    this.get("dialogs").pushObject({ id: id, presenter: presenter });
  },

  /**
   * @method
   * @param {module:ember-dialog/components/presenter} presenter
   */
  remove(presenter) {
    if (this.get('isDestroyed')) return;
    var id = presenter.get("presenterId") || guidFor(presenter);
    var dialogs = this.get("dialogs").filter((item) => {
      return item.id !== id;
    });
    this.set("dialogs", Ember.A(dialogs));
  },

  /**
   * @method
   * @param {module:ember-dialog/components/presenter} presenter
   */
  created(presenter) {
    this.add(presenter);
    presenter.one("declined", presenter => this.accepted(presenter));
    presenter.one("accepted", presenter => this.declined(presenter));
  },

  /**
   * @method
   * @param {module:ember-dialog/components/presenter} presenter
   */
  destroyed(presenter) {
    this.remove(presenter);
  },

  /**
   * @method
   * @param {module:ember-dialog/components/presenter} presenter
   * @listens module:ember-dialog/components/presenter~accepted
   */
  accepted(presenter) { this.destroyPresenter(presenter); },

  /**
   * @method
   * @param {module:ember-dialog/components/presenter} presenter
   * @listens module:ember-dialog/components/presenter~declined
   */
  declined(presenter) { this.destroyPresenter(presenter); },


  /**
   * @method
   * @fires module:ember-dialog/services/dialog~destroyAllPresenter
   */
  destroyAllPresenter(){
    this.get("dialogs").forEach(dialog => this.destroyPresenter(dialog.presenter));
  },
  /**
   * @method
   * @fires module:ember-dialog/services/dialog~destroyed
   * @param {module:ember-dialog/components/presenter} presenter
   */
  destroyPresenter(presenter) {

    presenter.destroy();
    /**
     * Triggered when `presenter` destroyed. You may subscribe on this event to
     * make additional operations.
     *
     * @example
     *
     * export default Ember.Component.extend({
     *
     *   dialog: Ember.inject.service(),
     *
     *   didRender() {
     *     this.get("dialog").on("created", { className } => {
     *       this.$().addClass("__freeze-content");
     *       className && this.$().addClass(className);
     *     });
     *     this.get("dialog").on("destroyed", { className } => {
     *       this.$().removeClass("__freeze-content");
     *       className && this.$().removeClass(className);
     *     });
     *   }
     *
     * });
     *
     * @event module:ember-dialog/services/dialog~destroyed
     * @type {module:ember-dialog/components/presenter}
     */
    Ember.run.scheduleOnce("destroy", this, () => { this.trigger("destroyed", presenter); });

  },

  /**
   * @example
   * <caption>
   * Simple usage - post factum handling. It's the common case when you needed
   * just get result of the dialog closing. For example show user an inform
   * message or ask him to confirm an action.
   * </caption>
   *
   * export default Ember.Controller({
   *
   *   success() {
   *     // Showing user modal with message that record was successfully deleted.
   *     // Showing template `app/templates/messages/success-deletion.hbs`.
   *     this.get("dialog").show("dialog/alert", "messages/success-deletion");
   *   },
   *
   *   deleteRecord(record) {
   *     // Showing user a dialog window to confirm act. Template to show the
   *     // user `app/templates/messages/areyousure.hbs`. The template
   *     // should have few buttons: "Ok" and "Cancel". "Ok" button evaluates
   *     // `accept` action, "Cancel" evaluates `decline` action.
   *     const promise = this.get("dialog").show("dialog/confirm", "messages/areyousure");
   *
   *     // User pressed "Ok" button - promise resolved in this case
   *     promise.then(() => { record.deleteRecord(); return record.deleteRecord(); });
   *
   *     // Record was successfully deleted
   *     promise.then(() => { this.success(); });
   *
   *     return promise;
   *   }
   *
   * });
   *
   *
   * @example <caption>Passing a context to the template that will be shown to
   * user in the dialog.</caption>
   *
   * export default Ember.Controller({
   *
   *   username: Ember.computed.alias("session.username"),
   *
   *   hello() {
   *     const username = this.get("username");
   *
   *     // Showing user simple personal greeting. The username passed in
   *     // context object. This object's put into the `contextObject`
   *     // presenter's property and become available in the template.
   *     this.get("dialog").show("dialog/alert", "messages/hello", { username });
   *   }
   *
   * });
   *
   * @example <caption>The template in this case will look like.</caption>
   *
   * Hello, {{contextObject.username}}!
   *
   *
   * @example <caption>With context you're able to pass an action names of the
   * context that will be executed at first, before executing `accept` and
   * `decline` handlers of the dialog (presenter).</caption>
   *
   * export default Ember.Controller({
   *
   *   showForm() {
   *     const options = { actionName: "save" };
   *
   *     // Passing current controller as context and the name of it's action
   *     // on `accept`.
   *     const promise = this.get("dialog").show("dialog/confirm", "messages/user-form", this, options);
   *
   *     promise.then(() => {
   *       console.log("Model saved");
   *     });
   *   },
   *
   *   actions: {
   *
   *     save(presenter) {
   *       const model = this.get("model");
   *
   *       if ( Ember.isBlank(model.get("username")) ) {
   *         this.get("dialog").show("dialog/alert", "messages/error");
   *         return;
   *       }
   *
   *       // "Manually" closing `presenter`
   *       model.save(() => presenter.accept());
   *     }
   *
   *   }
   *
   * });
   *
   * @method
   * @fires module:ember-dialog/services/dialog~created
   * @param {String|HTMLBarTemplate} layout    - Path to layout that used to showing message. Predefined layouts:
   *                                             `dialog/alert`, `dialog/confirm` and `dialog/blank`. Alert
   *                                             layout has only one button and can be closed as accepted only.
   *                                             Confirm layout has two buttons and can be close as accepted or
   *                                             declined. The blank layout hasn't any buttons at all and can
   *                                             be closed as accepted or declined. In any layouts available
   *                                             actions: 'accept' and 'decline'.
   * @param {String|HTMLBarTemplate} template  - Path to template that will be shown in the dialog window.
   *                                             In the template is available `presenter` object as `this`
   *                                             and context that passed on creation as `contextObject`.
   * @param {Object} [context]                 - An onject available in the template as `contextObject`.
   * @param {Object} [options={}]              - An object pass to presenter on creating.
   * @param {String} [componentName="presenter"]  - The dialog component name
   * @return {external:Ember/RSVP/Promise}
   */
  show(layout, template, context, options = {}, componentName = DEFAULT_COMPONENT_NAME) {

    /* Generate presenterId from (layoutName + templateName) or provided id
       to make sure the dialog won't open multiple times */
    var presenterId = options.id || "";
    if(typeof layout === "string" && typeof template === "string"){
      presenterId = layout + "/" + template;
    }

    delete options.id;
    //check if dialog is already opened
    var presenterItem = this.get("dialogs").findBy("id", presenterId);
    if(presenterItem && presenterItem.presenter){
      return new Ember.RSVP.Promise((resolve, reject) => {
        presenterItem.presenter.reopen({ resolve, reject });
      }, "Dialog #" + presenterId + " promise");
    }

    // Getting presenter instance.
    var presenter = getOwner(this).lookup(["component", componentName].join(":"));
    Ember.assert("You have passed `componentName` argument, but component by this name doesn't exist.", presenter);

    presenter = presenter.reopen(ContextMixin);
    presenter = presenter.reopen({presenterId: presenterId, target: context});
    options = Ember.merge(Ember.copy(this.get("defaults")), options);
    if (Ember.typeOf(layout) === "object") {
      options = Ember.merge(options, { layout: layout });
    } else {
      options = Ember.merge(options, { layout: getOwner(this).lookup(["template", layout].join(":")) });
    }

    if (Ember.typeOf(template) === "object") {
      // The template will be included into the presenter's body as
      // dialog-body component
      options = Ember.merge(options, { template: template });
    } else {
      // The template will be included into the presenter's body as partial
      options = Ember.merge(options, { templateName: template });
    }

    presenter = presenter.reopen(options);
    presenter.set("contextObject", context || Ember.Object.create());

    presenterId = presenterId || guidFor(presenterId);
    this.get("dialogs").pushObject({ id: presenterId, presenter: presenter });
    // Show it to user
    Ember.run(() => presenter.appendTo(options.root || this.get("rootElement")));

    /**
     * Triggered when `presenter` instance created. May be used to control
     * presenters outside the class.
     *
     * @example
     *
     * export default Ember.Component.extend({
     *
     *   dialog: Ember.inject.service(),
     *
     *   didRender() {
     *     this.get("dialog").on("created", { className } => {
     *       this.$().addClass("__freeze-content");
     *       className && this.$().addClass(className);
     *     });
     *     this.get("dialog").on("destroyed", { className } => {
     *       this.$().removeClass("__freeze-content");
     *       className && this.$().removeClass(className);
     *     });
     *   }
     *
     * });
     *
     * @event module:ember-dialog/services/dialog~created
     * @type {module:ember-dialog/components/presenter}
     */
    Ember.run.scheduleOnce("sync", this, () => { this.trigger("created", presenter); });

    return new Ember.RSVP.Promise((resolve, reject) => {
      presenter.reopen({ resolve, reject });
    }, "Dialog #" + presenterId + " promise");

  },

  /**
   * @example
   * <caption>
   * It is sugar. The [show]{@link module:ember-dialog/services/dialog~show} method with predefined layout.
   * </caption>
   *
   * export default Ember.Controller({
   *
   *   expired() {
   *     try { 1/0 } catch (e) {
   *       this.get("dialog").alert("messages/fatal-error", { text: e.message });
   *     }
   *   }
   *
   * });
   *
   * @method
   * @param {String} template
   * @param {Object} [context]
   * @param {Object} [options]
   * @param {String} [componentName="presenter"]  - The dialog component name
   * @return {external:Ember/RSVP/Promise}
   */
  alert(template, context, options, componentName = DEFAULT_COMPONENT_NAME) {
    var scope = this;
    if (!this.show) { scope = this.get("dialog"); }
    const layout = Configuration["ember-dialog"].layoutPath + "/alert";
    return scope.show(layout, template, context, options, componentName);
  },

  /**
   * @example
   * <caption>
   * It is sugar. The [show]{@link module:ember-dialog/services/dialog~show} method with predefined layout.
   * </caption>
   *
   * export default Ember.Controller({
   *
   *   remove() {
   *     const yes = () => { console.log("yes"); },
   *           no = () => { console.log("no"); }
   *     this.get("dialog").confirm("messages/yousure").then(yes, no);
   *   }
   *
   * });
   *
   * @method
   * @param {String} template
   * @param {Object} [context]
   * @param {Object} [options]
   * @param {String} [componentName="presenter"]  - The dialog component name
   * @return {external:Ember/RSVP/Promise}
   */
  confirm(template, context, options, componentName = DEFAULT_COMPONENT_NAME) {
    const layout = Configuration["ember-dialog"].layoutPath + "/confirm";
    return this.show(layout, template, context, options, componentName);
  },

  /**
   * @example
   * <caption>
   * It is sugar. The [show]{@link module:ember-dialog/services/dialog~show} method with predefined layout.
   * </caption>
   *
   * export default Ember.Controller({
   *
   *   showForm() {
   *     this.get("dialog").blank("forms/edit-user", this);
   *   }
   *
   * });
   *
   * @method
   * @param {String} template
   * @param {Object} [context]
   * @param {Object} [options]
   * @param {String} [componentName="presenter"]  - The dialog component name
   * @return {external:Ember/RSVP/Promise}
   */
  blank(template, context, options, componentName = DEFAULT_COMPONENT_NAME) {
    const layout = Configuration["ember-dialog"].layoutPath + "/blank";
    return this.show(layout, template, context, options, componentName);
  }

});