I realised, we cannot implement the above using routes. So I moved the logic in a Service which can manage the rendering process and take care of the query param management if a user navigate with the browser’s history back button.
Unfortunately, we can render in outlet only from route handlers and we can manage query params only from controllers… so this service uses the application route handler and controller to deal with this features.
You need a query param in the application controller:
// app/controllers/application.js
import Controller from 'ember-controller';
export default Controller.extend({
queryParams: ['showDialog'],
showDialog: false
});
and a named outlet in your application.hbs
:
{{!-- app/templates/application.hbs --}}
{{outlet}}
{{outlet 'modal'}}
This is the service:
// app/services/dialog.js
import Service from 'ember-service';
import getOwner from 'ember-owner/get';
export default Service.extend({
applicationRoute() {
return getOwner(this).lookup('route:application');
},
applicationController() {
return getOwner(this).lookup('controller:application');
},
open(template, model) {
this.applicationRoute().render(template, {
outlet: 'modal',
into: 'application',
controller: template,
model
});
// Setup the query param and watching it, this will be called
// when a user uses the browser's back button
const appCtrl = this.applicationController();
appCtrl.set('showDialog', true);
appCtrl.addObserver('showDialog', () => { this.close(); });
},
close() {
this.applicationRoute().disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
// Remove the query param watcher
const appCtrl = this.applicationController();
appCtrl.removeObserver('showDialog');
appCtrl.set('showDialog', false);
}
});
You can inject this service in your app, the following injects everywhere:
// app/initializers/dialog.js
export function initialize(application) {
application.inject('route', 'dialog', 'service:dialog');
application.inject('controller', 'dialog', 'service:dialog');
application.inject('component', 'dialog', 'service:dialog');
}
export default {
name: 'dialog',
initialize
};
We still need a modal component, which just a style wrapper and has a closing x in the corner. Let’s call it modal-dialog
component. This dialog has a close button and a close
action which calls directly the close()
method in our service:
actions: {
close() {
this.dialog.close();
}
}
Now you can have more templates and controllers which represents different content and modals, you can call these templates and controllers from everywhere in your application, they will always rendered on top of the actual route and content.
Let’s say you have a button which would open an instant-help
form/template with this button:
<button class="btn btn-default" {{action "instantHelp"}}>Instant Help</button>
That instantHelp
action will buble up, so somewhere in a parent route level has to have an action which would call the dialog service and open it:
actions: {
instanHelp() {
this.dialog.open('instant-help');
}
}
Now, you can open the dialog with clicking on a button, and close the dialog with a ui button, or with the back button in the browser/mobile. But the dialog will not open again if you click on the forward button after, so it is safe.
A demo repository where I experiment with this approach: ember-dialog-service-example/dialog.js at master · zoltan-nz/ember-dialog-service-example · GitHub
Ember Twiddle Link