Modal Views - can we agree on a best practice?

This is one of those ubiquitous things (like pagination) without a really good canonical example. Here are a few cases that come to mind. I’m not saying all of these are good options, I’m trying to show different approaches that come to my mind.

Option 1 - create the modal view manually

User clicks on a button and a modal is supposed to appear

showDeleteConfirmationModal: function(user) {
  modal = Ember.View.create({
    templateName: "something",
    controller: this.controller,
    content: user,
    popupHide: function() {
      this.remove();
    }
  }).appendTo('body');
}

This isn’t really nice, since the modal isn’t a self-contained thing, we kind of have to hack the view’s controller and content and manually append it to the body.

Option 2 - create the thing before hand and display it by triggering an action

We could also render all the modals that could appear on the page by doing something like

{{render "confirmation_modal"}}

and then the action handler might look something like this

App.SomeController = Ember.Controller.extend({
  needs: "confirmation_modal",

  showDeleteConfirmationModal: function() {
    this.get("controllers.confirmation_modal").show();
  }
});

Option 3 - global binding to display the modal

This is somewhat similar to the previous example, we would pre-render the modal, but only display it based on a binding

{{#if App.confirmationModal}}
  {{render "confirmation_modal"}} // rendered in the application.hbs
{{/if}}

and the action handler would just change the global state that triggers the modal

showDeleteConfirmationModal: function() {
  App.set("confirmationModal", true);
}

Option 4 - transition to a new substate which renders the modal in an outlet

There’s not much really to show here. Whenever we want to display the modal view we would transition into a substate which would render the modal.

showDeleteConfirmationModal: function() {
  this.transitionToRoute("something.confirmation", this.get("content"));
}

Option 5 - local binding

We could do something similar to option 3 but instead render the modals only on the page on which they belong. Other than that, this is basically identical.

Conclusion

In retrospective after writing this, I’m not sure if I like any of these options

  1. probably good enough for simple things, but it forces us to put all the logic in the view
  2. what happens here if we can’t get to the correct instance via needs?
  3. making every single modal global in the application feels wrong
  4. this could work for fullscreen modals, but what about simple popovers?
  5. what if we want to pass in a specific context when displaying the modal?

Personally I’ve done mostly 1., but I’m not happy about it. I’d love to hear how other people are solving this, and if possible we could come up with a canonical example that could make it into the documentation (maybe?) :slight_smile:

23 Likes

There’s one option that I’m using not mentioned above:

Use route#render method

This gives you the advantage of Option 1 of being able to optionally perform operations before rendering, and easily customize the modal based on the specific case. The advantage of Option 2 of having the self-contained thing. The advantage of Option 3 by only displaying when needed.

App.ApplicationRoute = Ember.Route.extend({
  events: {
    showModal: function(options) {
      this.controllerFor('modal').setProperties(options);
      this.render('modal', {
        into: 'application',
        outlet: 'modal'
      });
    },

    dimissModal: function() {
      this.controllerFor('modal').reset();
      this.clearOutlet('application', 'modal');
    }
  },

  clearOutlet: function(container, outlet) {
     parentView = this.router._lookupActiveView(container);
     parentView.disconnectOutlet(outlet);
   }
});
15 Likes

Some good ideas on this topic was brought up in this thread about Discourse’s modals.

Elegantly formulates the lack of mapping to a “higher state” which I think is true for many modal scenarios.

Brings up the usage of how StateMangers could be used for modals. I think there might be a lot of cool stuff that could be done with state machines for both simple and more complex, wizard flow kind of, modals.

I think this part of Peter Bergströms “Ember.js in the Wild” (slides) talk shows some really cool use cases for StateManagers which would marry well with modals.

Another requirement on modals should be that they need to work well with browser history (back/forward buttons). E.g. Discourse’s modals remain on screen even when you navigate away from the context that triggered them.

Without putting too much thought behind it I recall this issue where @pwagenet talks about having unconnected outlets (taken out of context):

What I’m saying is that it doesn’t seem to me that you should ever have an unconnected outlet in a state. Therefore, when you switch into a new state, the outlet would always be overridden anyway.

and

IMO, having an unpopulated outlet is bad design. However, if you really feel the need to do that, I would set the outlet property (default is view) on the controller to null.

However disconnectOutlets has obviously been added since. Maybe the philosophy around outlets have changed. It would be would be great to hear @pwagenet or some other knowledgeable person’s thoughts on this.

I like @darthdeus’ approach number 1 the most. That is, creating the modal programmaticaly. A modal often doesn’t have its own state in the router, and it’s self-contained, so blending it in with outlets seems a little weird.

The modal should consist of a view and a controller though. And it should be trivial to connect the two. Preferrably Ember should do it for you.

Proposal: Ember.control(name, model, properties)

I think a really good solution would be to make it possible to programmatically do what the {{control}} handler does for templates.

This could be done by adding a new method to Ember. It could be named Ember.control, and should take three arguments:

  • name: Which, just like with routes, {{render}}, and {{control}} infers which controller, view and template should be used. name = 'deleteUser', would use App.DeleteUserController, App.DeleteUserView and the view’s templateName will default to delete_user.
  • model: An optional object, which will be set as the model property on the controller.
  • properties An optional hash of extra properties to be set on the controller.

When Ember.control is invoked, Ember should do the following:

  • Instantiate a controller and a view of the classes determined by the general naming convention and name. The controller is not a singleton, just like with {{control}}, as we might want to have multiple modals with the same controller class, but different models.
  • Set the model object as the controller’s model property.
  • Set the view as the controller’s view property.
  • Apply each key in properties to the controller. They should not be set on the view, because all logic and data storage belongs in controllers.
  • Return the controller. The controller should be returned, because:
    • There is no reason to create another object to be returned.
    • If the app needs access to the view, it can do so through the controller’s view property.

A use case for Ember.control can be seen here:


//This is the /users route's controller, whose template has a button with an action that calls `showDeleteConfirmationModal`
App.UsersIndexController = Ember.Controller.extend({
  showDeleteConfirmationModal: function(user) {
  	//This is how you instantiate a control, just like {{control}} would do it in a template
  	var deleteUserController = Ember.control('deleteUser', user, {
		something: 111 //These properties goes on the controller
  	});
  	//`deleteUserController` would normally be self-contained, so normally you don't need to manipulate it, but you get the controller instance here in case you need to close it programmatically from somewhere else.
});

//In this app we have modals, where the controller should implement this mixin to support closing the window
App.ModalControllerMixin = Ember.Mixin.create({
	close: function() {
		this.willClose();
		this.get('view').destroy();
		this.destroy();
	},
	willClose: Em.K
});

//Specific modal views should extend this, so they get wrapped in a div, gets a close button etc.
App.ModalView = Ember.View.extend({
	layout: 'modal',
	classNames: ['modal'],
	init: function() {
		this._super();
		this.append();
		//Should probably check with some manager instance what zindex it should have, create mask element, etc.
	}
})

//This is a concrete modal controller.
//We can put all logic in a controller, where it belongs
App.DeleteUserController = Ember.ObjectController.extend(App.ModalControllerMixin, {
	isDeletable: function() {
		return this.get('model.organizations.length') == 0;
	}
});
//This is a concrete modal view
//We can put view events in the view, where they belong
App.DeleteUserView = App.ModalView.extend({	
});

I think this is a very clean solution. I only need to define the generic behavior of modals in my app once. From there on all I need to do to implement a new concrete modal is to define a controller, a view, and a template, and call Ember.control('someName', someObject). And any of the controller, view and template are optional, just like they are with routes.

Ember.control would be useful for anything that’s not part of the common route handling, and has “it’s own life”. Popover menus, dropdown menus, combobox selector panels, etc.

The implementation of Ember.control would be rather simple. I would love to make a pull request to the repo, if people agree with me that this is a nice design.

What do people think?

I would like to include @wycats and @tom in this discussion as I think this would be a vital feature of Ember.

4 Likes

+1 for @seilund proposal.

For {{controll}} and {{render}} I think allowing properties would be a really useful as described here: https://github.com/emberjs/ember.js/issues/1914 and here: https://github.com/emberjs/ember.js/pull/2225

Going a little bit further I think {{control}} and {{render}} should allow body templates. For the modal case, it would look like that:

{{#control Modal titleBinding="name"}}
   <p>The body of the modal view</p>
   <p>It gets data from your main controller: {{name}}</p>
{{/control}}

What do you think? It should work exactly like {{view}} but for controller+view+template. Where template can be defined inline as shown above.

1 Like

I guess that makes sense too, especially because the {{view}} helper also has a block variant. I haven’t ran into cases where I would have needed a body in {{render}} or {{control}} though.

I just submitted a PR with Ember.control: https://github.com/emberjs/ember.js/pull/2490

I’m using a variant of it in our own app, and it works beautifully.

5 Likes

I like the strategy of the control function. Whatever is done, I’d want it to be compatible with other web frameworks, such as Zurb Foundation’s Reveal plugin. I’m still green on Ember so I need to noodle on how that would be achieved.

I wanted to give the rendering a modal into an outlet a try so I whipped up this example.

http://ember-examples-modal.herokuapp.com/ https://github.com/nerdyworm/ember-examples-modals

I don’t know if this is a best practice but it actually feels OK.

I am able to animate the opening and closing of the modal, wait until the server acknowledges the create/update/delete to close, and rollback transactions.

Going to try to whip up a few more examples of the other ways that have been posted here just for comparison.

I’ve been trying to follow the way that was shown in the presentation @ashaegupta did but am having trouble connecting the state manager states to controllers and views. I cant figure out how to use the new router to do a connectOutlets. Any tips?

I’m hoping the PR that @seilund submitted will get put into master sooner rather than later but in the mean time I need a work around and the stateManager way seemed the cleanest with my codebase.

I’ve written up how I usually approach this in the past.

The approach I outline assumes that there is a corresponding route for what the modal contains. This has some benefits, but is not appropriate for all use cases.

The benefits include what to my mind is a clear separation of concerns.

  • How the form is displayed (as a modal) is a detail of the styling in the template.
  • The triggering of the showing or hiding of the modal is done by the view code where user events are meant to be handled.
  • State change and data handling are done in the route or controller and actually don’t need to know anything about the implementation details of presentation.

On further reflection though, I’m not sure that we should be striving for only one canonical technique for modals. The right modal technique depends on the modal’s purpose (does it trigger manipulation of data or is it just a point of information for the user? and should be thought of with that in mind.

Maybe the best way forward is to put together a set practices for types of modals.

If we settle on only one, we might slip into some of the pitfalls of earlier SproutCore stuff.

1 Like

It’s been almost a month since this was last discussed, has anyone found new ways of working with modals/overlays or run into issues with any of the above methods?

I’m using a variation of @teddyzeenny’s method above (Modal Views - can we agree on a best practice? - #2 by teddyzeenny) but I’m running into issues with transactions - Within the modal controller, if I create a record inside a transaction and then roll the transaction back when closing I’m still left with the empty object in the data store.

1 Like

I have exactly the same problem. I’d love to know what I was doing wrong and how to do it right.

@lookingsideways, @sambeau this looks like an ED bug. Did you try deleting the record before rollback?

Calling .deleteRecord() on the model before rolling back still leaves the empty record in the data store.

I’ll see if I can create a JSFiddle that demonstrates the issue.

Update: The fiddle is up here Edit fiddle - JSFiddle - Code Playground but somehow it works there! I’ll see if I can continue fleshing it out to see if I can get it to exhibit the in-app issue I’m having.

@sambeau Are you using any 3rd party libraries such as ember-auth? Just seeing if I can nail down some possibilities as my transactions are working fine inside limited test environments.

I think I’ve narrowed my issue down to something that’s not related to modals - creating a model and rolling it back in the console puts the record into a deleted state but still shows a blank entry on my list view.

Has anyone run into a similar issue? Not sure where to start debugging this.

Sorry for sidetracking the conversation a bit here. I ended up having to add {{#unless isDeleted}} into my #each loop to avoid the blank models being rendered. This isn’t normal is it?

thanks a lot @teddyzeenny for this example! I implemented my global modal dialog by taking your example and tweek it so that it would satisfy my needs and it works like a charm!

@jacobk did you hear something about the topic of having unconnected outlets which you could share with us?

1 Like