CiviCRM AngularJS extension

a pixelated screenshot of a terminal window

We support Campaign Against Arms Trade, a right-on group that works to end the international arms trade, with their technology – including CiviCRM, a popular open source “constituent relationship management” platform.

Among other things, CAAT uses CiviCRM’s “Mailing” features to send out emails to their supporters, and they told us that they’re experiencing an annoying bug: if a user sends out a mailing with the same name (not subject line, just the internal identifier 🙄) as an existing one, it’ll cause the CRM to freeze up elsewhere.

As an added challenge, the mailing features of CiviCRM now use AngularJS following a recent rebuild by the developers, and there aren’t many tutorials or examples out there to customise it. Luckily, the CiviCRM developer community was super-helpful, and we managed to sort out some in-form validation to prevent duplicate mailing names for our client… and now for you, too!

Create a new extension

Using civix, we set up a new CiviCRM extension for our code:

$ civix generate:module mailing

(We called our extension mailing, because our creative director was occupied with an Art at the time)

Our client sensibly keeps their custom CiviCRM extensions in git, so at this stage we initialised a repository, added the boilerplate template code, and pushed.

Set up the AngularJS hook

A function called mailing_civicrm_alterAngular() will get executed whenever an AngularJS page loads, and you can use a ChangeSet to edit an AngularJS template. Because AngularJS templates specify form logic, this also lets you change the validation behaviour. Our hook function looks like this:

function mailing_civicrm_alterAngular($angular) {
  $changeSet = \Civi\Angular\ChangeSet::create('mailing_name_unique')
    ->alterHtml('~/crmMailing/BlockSummary.html', function(phpQueryObject $doc) {
      // name validation
      $doc->find('.crm-group:has([crm-ui-id="subform.mailingName"])')->attr('ng-controller', 'NameValidateCtrl');
      $doc->find('[crm-ui-id="subform.mailingName"]')->attr('ng-blur', 'validateName(mailing, \'name\')');
      $doc->find('[crm-ui-id="subform.mailingName"]')->attr('crm-ui-validate', 'isValid');
    });

  $angular->add($changeSet);

  CRM_Core_Resources::singleton()->addScriptFile('mailing', 'js/disallow-duplicate-names.js');
}

Setting crm-ui-validate to validateName directly fired the event way too many times, so instead validateName is only called when focus leaves the field, ng-blur, which then sets the isValid variable that’s checked by crm-ui-validate.

Create the validateName function

Then, the code which queries the CiviCRM API to check for mailings with duplicate names:

var validating = false;

(function(angular, $) {
  var crmMailing = angular.module('crmMailing');

  crmMailing.controller('NameValidateCtrl', function($scope) {
      $scope.isValid = false;

      $scope.validateName = function(mailing, field) {
        if (!validating) {
          validating = true;

          CRM.api3('Mailing', 'get', {
            "sequential": 1,
            "name": mailing[field],
            "id": {"!=": mailing.id}
          }).then(function(result) {
            // do something with result
            if (result.count > 0 ) {
              $scope.isValid = false;
              CRM.alert(ts('There is already a mailing with this name; sending this one will crash CiviCRM!'));
            } else {
              $scope.isValid = true;
            }
          }, function(error) {
            // oops
            console.log(error);
          });

          validating = false;
      }
    };
  });
})(angular, CRM.$);

(saved as js/disallow-duplicate-names.js)

Conclusion

Activate the extension, e.g. with cv

$ cv en mailing

Now, open a mailing and try to give it the same name as an existing one – you should see the field border turn red, and you’ll be prevented from continuing or sending the mailing:

"Mailing name" field showing the field with a red border and red label

(As a bonus, the extension also sends a notification to the user using CRM.alert to explain the error)

It does seem like a red border sometimes hangs around the field label even after the value is valid again… but apart from that, the feature is working great!

Lastly, props to CAAT for being a great member of the CiviCRM community and supporting us writing this post to share our work with y’all.