Configuring AngularJS 3rd party module

How to use provider to allow user modules to configure 3rd party features.

Demo, source

I showed how to inject any configuration constants into an AngularJS application using server-side templates in Inject valid constants into Angular blog post. In this blog post I will show how to allow users to configure your module, if they use it as a 3rd party dependency.

Take my confirm-click module as an example. It is a tiny single attribute directive that shows a confirmation "Ok / Cancel" dialog before allowing ng-click or href event to proceed. One can simply add this to the markup

1
<button confirm-click="Submit this form?" ng-click="submit()">Submit</button>

Under the hood, confirm-click directive replaces ng-click attribute with its own utility function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// confirm-click directive compile function
var config = {
ask: window.confirm.bind(window) // 1
};
if (attributes.ngClick) {
attributes.prevClick = $parse(attributes.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true);
attributes.ngClick = '__confirmClick' + counter++ + '()';
}
// ...
// add __confirmClick<id> utility method to the scope
var methodName = '__confirmClick<id>';
scope[methodName] = function (event) {
$q.when(config.ask(question)).then(function (result) { // 2
if (result) {
attr.prevClick(scope, { $event: event });
}
});
};

By default we just use window.confirm method (line // 1) when asking the user before proceeding (line // 2). Ordinary alert, prompt and confirm dialogs look ok, but they definitely do not match my website's visual style. Instead we use a customized version of alertify.js library. This library provides the same methods but made via actual modal dialogs; these can be customized using CSS. We even have written a AngularJS wrapper kensho/ng-alertify that wraps alertify.confirm and alertify.prompt into promise-returning methods.

1
2
3
4
5
6
7
angular.module('MyApp', ['Alertify'])
.run(function (Alertify) {
Alertify.confirm('Are you sure?').then(
function onOk() {...},
function onCancel() { ... }
);
});

How can we make our confirm-click module use the Alertify.confirm or any other user-supplied function instead of the default window.confirm?

Using AngularJS provider to pass config to the directive

We can set the desired ask function via a Provider instance. Let us add a provider directly to the ConfirmClick module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('confirm-click', [])
.directive('confirmClick', ...); // line 1: confirm-click directive
.provider('ConfirmClick', function () {
var config = {
ask: window.confirm.bind(window)
};
return {
set: function (options) {
config.ask = options.ask || config.ask;
},
$get: function () {
return config;
}
};
});

We can inject ConfirmClickProvider anywhere, including the directive ConfirmClick (line // 1).

1
2
3
4
5
angular.module('confirm-click', [])
.directive('confirmClick', function (ConfirmClick) {
// ConfirmClick is the `var config` object returned by `$get` above
// ConfirmClick.ask(question) is window.confirm(question) by default
});

When we inject ConfirmClick into the directive, we get the result of the provider's ConfirmClick.$get() method. In our case it will be the window.confirm by default.

Setting config via provider from the user module

Let us see how to change the ask function from our application code. Our application is the module ClickApp that depends on confirm-click and Alertify modules.

1
2
3
4
5
6
7
8
9
angular.module('ClickApp', ['confirm-click', 'Alertify'])
.config(function (ConfirmClickProvider) { // 1
ConfirmClickProvider.set({
ask: function (question) {
var Alertify = angular.element(document.body).injector().get('Alertify'); // 2
return Alertify.confirm(question);
}
});
});

Notice that we use the module.config method to inject an instance of the provider in line // 1 (only providers are allowed besides constants to be injected into the module's config callback). The injected ConfirmClickProvider object has both set and $get method we have written, thus we can pass our own ask function. In this case the custom function needs to get an instance of Alertify. I found grabbing the injector instance directly to get Alertify the simplest way.

The result looks nice even without any additional styling

confirm screenshot