Extract $mdDialog and $uibModal into dedicated services

Angular Material, Angular Bootstrap and other UI Libraries ship with something that used to be called “Pop Up”. These widgets usually pop up in the center of the screen with a dark backdrop. Material calls it “Dialog” while Bootstrap denotes it “Modal”. However, these overlays are handy to show detailed information about an entity and allow in-place manipulation.

Datepicker inside of Material Dialogs (source: Material Design Spec)

In this article, I want to explain why you should create dedicated services to launch dialogs and modals in order to keep your code clean, testable and maintainable.

Coupling of top-level parts

In a typical Angular application, top-level controllers and templates are bound by a routing module (legacy ngRoute, fancy ui.router or new migration-friendly Component Router). The following example shows how to define a new state and wire together url, template and controller using ui.router.

$stateProvider
.state('fooState', {
url: '/foo',
templateUrl: 'components/foo/foo.html',
controller: 'FooController',
controllerAs: 'foo',
})
// define more states with chained execution

The definition at a central place like the state configuration provides a single source of truth (SSOT). Aiming a SSOT architecture simplifies maintenance because later changes just have to be applied to a single location.

Coupling of lower-level parts

The Angular core ships with the directives ng-controller and ng-include. Unfortunately, these directives mix up wiring with template code. That’s why top-level parts are wired with routers. For lower-level parts, custom Angular directives and the new components should be used. The result is clean code with increased modularity and better reusability.

angular
.module('exampleApp')
.component('foo', {
templateUrl: 'components/foo/foo.html',
controller: 'FooController',
bindings: {
entity: '=',
callback: '&'
}
});

Coupling of dialogs and modals

$mdDialog and $uibModal are powerful services which can be customized with many options. Specifying just half of the possible options can produce quite verbose code. Therefore, having dialog and modal launchers in your controllers is a bad idea. It will mess up your controller. The next example shows the disadvised invocation of a dialog with minimal configuration inside of a controller.

angular
.module('app')
.controller('FooController', function($mdDialog) {
this.showBarDialog = showBarDialog;
    function showBarDialog() {
$mdDialog.show({
templateUrl: 'components/bar/bar.html',
controller: 'BarController',
controllerAs: 'bar'
});
}
});

The solution: Move the dialog invocation into a dedicated service. It cleans up the controller, increases the testability of the dialog component and creates a single source of truth (cf. top-level and lower-level parts above).

angular
.module('app')
.factory('barDialog', function($mdDialog) {
return showBarDialog;
    function showBarDialog() {
return $mdDialog.show({
templateUrl: 'components/bar/bar.html',
controller: 'BarController',
controllerAs: 'bar'
});
}
});
angular
.module('app')
.controller('FooController', function(barDialog) {
this.showBarDialog = barDialog;
});

In general, the outcome of the promise-based dialog wants to be consumed especially when using the dialog as dumb component. In order to do this, callbacks have to be registered. The next example restores showBarDialog by invoking the extracted barDialog and registers promise callbacks.

angular
.module('app')
.controller('FooController', function(barDialog) {
this.showBarDialog = showBarDialog;
    function showBarDialog() {
barDialog().then(...) // register callbacks
}
});

As mentioned above, dialogs and modals come with many different configuration options. With a simple refinement, we can override the default configuration with a custom one. The secret is a call to angular.extend. For illustration, we decorate the dialog configuration with an event while keeping the remaining configuration as is. The event object is used by Angular Material to calculate a nice animation from the source of the event to the center of the screen where the dialog will be displayed.

angular
.module('app')
.factory('showBarDialog', function($mdDialog) {
return showBarDialog;
    function showBarDialog(config) {
var defaults = {
templateUrl: 'components/bar/bar.html',
controller: 'BarController',
controllerAs: 'bar'
}
return $mdDialog.show(angular.extend(defaults, config));
}
});
angular
.module('app')
.controller('FooController', function(barDialog) {
this.showBarDialog = showBarDialog;
    function showBarDialog(event) {
barDialog({event: event}).then(...) // register callbacks
}
});

Conclusion

  • Prefer a single source of truth for your controller/template bindings.
  • Loosely couple components to simplify testing and maintenance.
  • Keep your controllers as skinny and extract as much as possible logic.
  • Consider dedicated services for $mdDialogs and $uibModals.

Sebastian offers IT coaching and consulting at synsugar. He’s passionate about web technologies and likes to contribute to open source. Connect with him via @s_henneberg or @synsugarIT.

Like what you read? Give Sebastian Henneberg a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.