Salesforce chatter in AngularJS part 1

The requirement was a single page application built using AngularJS that hooks into salesforce’s chatter API with the ability to upload files. Here’s how it was done…

Overview

chatter

The basic idea is this.

  • A directive sits on the page and allows users to view a feed, post a message, link or select/drag/drop files on to a file upload area.
  • A service interacts with the salesforce chatter rest api to upload the data.

The chatter rest api expects the data to be sent in a very particular way including certain headers for authentication. This post deals with the directive for the chatter component. A future post will explain the service.

The directive contains functions to load the feed and parse feed elements, post messages and links and upload files with the required form validation. It also uses v-accordion to display the feed and ngFileUpload for handling file selection and validation.

Displaying the feed

Here’s the js for the getting the feed from the directive…

function getFeed () {
  return vfRestService.getFeed(vm.recordId)  // 1
    .then(function(results) {
      if (!angular.isArray(results.elements)) {
        vm.errorMessage = flashMessages.error;  // 2
        return;
      }
      vm.feed = results;
      vm.feed.elements = _.filter(vm.feed.elements, function (el) {
        return allowedFeedTypes.indexOf(el.type) > -1;
      });  // 3
      if (vm.feed.elements.length > 0) {
        vm.feed.elements[0].expanded = true;
      }
    })
    .catch(function (error) {
      $log.error(error);
      vm.errorMessage = flashMessages.error;
    });
}

Here’s a little run-down of this code

  1. The actual job of getting the feed is deferred to a service. In the case of getting the feed, the logic is fairly trivial.
  2. If the resulting data doesn’t contain an ‘elements’ node (which is returned by salesforce), something went wrong, so show an error message
  3. Filter the elements based on a set of allowed types. In this case, we’re using underscore to filter elements that are either TextPost’s, LinkPost’s or ContentPost’s.

Here’s the markup for the directive template…

<v-accordion class="vAccordion--chatter" control="vm.accordion">
<v-pane ng-repeat="el in vm.feed.elements" expanded="el.expanded" class="chatter--feed-pane" >
  <v-pane-header>
    <span ng-class="el.type.toLowerCase()" class="icon--chatter">(icon)</span>
    <span ng-bind="el.actor.displayName"></span>
   </v-pane-header>
   <v-pane-content ng-switch="el.type">
     <p class="note" ng-bind="el.header.text"></p>
           <div ng-switch-when="ContentPost" class="feed-item--content" ng-init="content = el.capabilities.content">
          <a ng-bind="content.title" ng-href="{{ vm.getFileDownloadUrl(content.downloadUrl) }}"></a>
          <p class="note">({{ content.fileType }} &ndash; {{ vm.getFileSize(content.fileSize) }})</p>
        </div>
        <div ng-switch-when="LinkPost" class="feed-item--link">
          <a ng-href="{{ el.capabilities.link.url }}" ng-bind="el.capabilities.link.urlName"></a>
        </div>

        <div class="feed-item--text" ng-if="el.body.text != null">
          <p ng-if="!el.body.isRichText" ng-bind-html="el.body.text"></p>
      <div ng-if=el.body.isRichText ng-bind-html="vm.getRichTextMessage(el.body.messageSegments)"></div>
        </div>

    </v-pane-content>
    <p class="date" ng-bind="el.createdDate | date:'dd/MM/yyyy HH:mm'"></p>
</v-pane>
</v-accordion>

So, the key bits here…

  • A pane is created in the accordion for each feed element. If the pane is the first one, its been marked as expanded.
  • The actual content shows either text, links or file output using the ‘ng-switch’ directive. All that is fairly straightforward. One interesting bit is the body text. If its rich text, salesforce returns it all in segments, so this needs parsing to actually output it as html. Here’s the code for that…

    function getRichTextMessage (messageSegments) { var rt = []; angular.forEach(messageSegments, function(segment){ switch (segment.type.toLowerCase()) { case ‘markupbegin’: rt.push(‘<‘ + segment.htmlTag + ‘>’); break; case ‘markupend’: rt.push(‘</’ + segment.htmlTag + ‘>’); break; default: rt.push(segment.text); break; } }); return rt.join(”); }

This little function loops through each section, checks the segment type and adds either a start or end html tag or the actual text content. Since each segment in the message is ordered correctly, just looping through the segments is sufficient to construct a valid bit of html.

Posting stuff

From the directive, the general method here is to validate the relevant form, and if valid, send the form data to the service, which then interacts with the chatter rest service and gets some kind of status back.

One point of interest is the file validation. This is achieved pretty easily using the ngFileUpload directives.

file-upload

<div ngf-select="" ngf-drop="" ngf-max-size="20MB" ng-model="vm.fileFile" name="fileFile" id="fileFile" required  ngf-multiple="false" ngf-allow-dir="false" class="chatter-file-drop" ngf-drag-over-class="'dragover'" ng-class="{ 'form-error' : vm.formHasError(vm.fileForm, 'fileFile') }">Select or drop a file</div>
  <div ng-show="vm.fileFile">
    <p>Pending file:</p>
    <p ng-bind="vm.fileFile.name"></p>
  </div>
      <div ng-show="vm.formHasError(vm.fileForm, 'fileFile')">
    <span role="alert" class="form-error__msg" ng-show="vm.fileForm.fileFile.$error.required && !vm.fileForm.fileFile.$error.maxSize"><b>!</b> File is required</span>
    <span role="alert" class="form-error__msg" ng-show="vm.fileForm.fileFile.$error.maxSize"><b>!</b> File is too large. 20MB is the maximum allowed file size</span>
  </div>
</div>

Using the directives, a file drop zone can be specified and validation added for file size, file type etc. Our implementation didn’t use all the options but did a pretty good job in the end. The only point of annoyance is that its not ie9 compatible. The ngFileUpload component does provide a ie8-9 flash fallback but I just couldn’t get it working. This is mainly because we’re not actually uploading files using ngFileUpload, we’re just making use of its drag/drop and validation capabilities. The actual file upload is done using a different method, compatible with the chatter api.

The directive part of the file upload process is as follows –

var reader = new FileReader();  // 1
reader.onload = (function (evt) {
  vfRestService.postFile(vm.recordId, evt.target.result, vm.fileFile.name, vm.fileMessage)  // 2
  .then (function (feedItem) {
    if (!angular.isDefined(feedItem.id)) {
      vm.postErrorMessage = flashMessages.error;
      return;
    }
    addToFeed(feedItem, vm.fileForm);  // 3
    vm.fileMessage = '';
    vm.fileFile = '';
    vm.pendingSubmit = false;
  })
  .catch (function () {
    vm.postErrorMessage = flashMessages.error;
    vm.pendingSubmit = false;
  });
});
reader.readAsArrayBuffer(vm.fileFile);
  1. We use the FileReader which is only available from ie10 and up (grr). However, the chatter service expects an arraybuffer to be added to the data sent in the post request.
  2. once the data has been read, its passed to the vfRestService along with the id, and a bunch of other stuff which is pretty self-explanatory.
  3. If the post worked, a feed element is returned including a url to the actual uploaded file. This is added to the feed that’s displayed in the accordion.

A little thing, the ‘pendingSubmit’ variable is used to set the ‘disabled’ state of the submit button, so that users can’t keep pressing it re-submitting the form millions of times.

The next part in this series will talk about the service required to post stuff into salesforce.

Leave a Reply