Dragula drag and drop in a Lightning Component

Dragula is a great library that makes adding drag and drop easy. It handles creating a copy of the element being dragged that moves with the cursor and also shows where the element will go in the drop area. It has a simple API and is self contained. (The Locker Service is happy with version 3.7.2 of Dragula that I used.)

Here is how I hooked it into a Lightning Component to produce this result:

Drag and drop screen shot

In the component’s controller, this code connects the DOM element oriented Dragula up with the component oriented Lightning straight after Dragula is loaded. There is a bit of extra logic to add and remove a placekeeper drop area when there are no items:

({
afterDragulaLoaded: function(component, event, helper) {

    // Components
    var container = component.find('container');
    var from = component.find('from-draggable');
    var to = component.find('to-draggable');

    // Dragula needs the DOM elements
    var drake = dragula([from.getElement(), to.getElement()], {
        direction: 'vertical',
        mirrorContainer: container.getElement()
    });

    // Show/hide the "Drag and Drop Here" item
    // $A.getCallback makes safe to invoke from outside the Lightning Component lifecycle
    drake.on('drop', $A.getCallback(function(el, target, source, sibling) {
        if (source.children.length <= 1) {
            $A.util.removeClass(component.find(helper.placekeeperAuraIdFor(source)), 'slds-hide');
        }
        $A.util.addClass(component.find(helper.placekeeperAuraIdFor(target)), 'slds-hide');
    }));
}
})

This helper is used:

({
placekeeperAuraIdFor: function(element) {
    // Hard to get from DOM back to aura:id so using classes as markers
    if (element.classList.contains('from-draggable')) return 'from-placekeeper';
    else if (element.classList.contains('to-draggable')) return 'to-placekeeper';
    else return null;
}
})

Here is the component markup: it is just hard-coded data and styling to keep this example simple and references the Dragula JavaScript and CSS static resources via a ltng:require at the bottom:

<aura:component >
    
<div aura:id="container">
    
    <div class="slds-text-heading--medium">Candidates</div>
    
    <ul aura:id="from-draggable" class="from-draggable">
        <li class="slds-p-around--xx-small">
            <article class="slds-card">
                <div class="slds-card__body">
                    <div class="slds-tile slds-tile--board">
                        <h3 class="slds-truncate" title="Anypoint Connectors">
                            <a href="javascript:void(0);">Anypoint Connectors</a>
                        </h3>
                        <p class="slds-text-heading--medium">$500,000</p>
                    </div>
                </div>
            </article>
        </li>
        <li class="slds-p-around--xx-small">
            <article class="slds-card">
                <div class="slds-card__body">
                    <div class="slds-tile slds-tile--board">
                        <h3 class="slds-truncate" title="Cloudhub">
                            <a href="javascript:void(0);">Cloudhub</a>
                        </h3>
                        <p class="slds-text-heading--medium">$185,000</p>
                    </div>
                </div>
            </article>
        </li>
        
        <li class="slds-p-around--xx-small slds-hide" aura:id="from-placekeeper">
            <div class="slds-file-selector__dropzone" style="height: 50px">
                <span class="slds-file-selector__text">Drag and Drop Here</span>
            </div>
        </li>
    </ul>
    
    <div class="slds-text-heading--medium">Selected</div>
    
    <div class="slds-panel slds-grid slds-grid--vertical slds-nowrap">
        <ul aura:id="to-draggable" class="to-draggable">
            <li class="slds-p-around--xx-small" aura:id="to-placekeeper">
                <div class="slds-file-selector__dropzone" style="height: 50px">
                    <span class="slds-file-selector__text">Drag and Drop Here</span>
                </div>
            </li>
        </ul>
    </div>
</div>

<ltng:require styles="{!$Resource.DragulaCss}"
              scripts="{!$Resource.DragulaJs}"
              afterScriptsLoaded="{!c.afterDragulaLoaded}"
              />
    
</aura:component>

This CSS is also used:

.THIS li {
    list-style-type: none;
}
.THIS article.slds-card:hover {
    border-color: #1589ee ;
}

There were two awkward areas:

  • As far as I know, there is no way to go from a DOM element back to an aura:id so in Dragula’s “drop” callback I resorted to using a marker class instead.
  • The $A.getCallback wrapping function is needed as explained in Modifying Components Outside the Framework Lifecycle.

Adding to the CKEditor context menu

We are using the excellent CKEditor in some of our Visualforce pages now, partly to get around the problem that standard rich text fields error when they are re-rendered. But explicitly adding the editor also makes it cleaner to leverage its many features and the one described here is the ability to extend the context (right click) menu. I couldn’t find a simple example, so I hope this post will act as that for others.

The JavaScript code is below and works on this text area:

<apex:inputTextArea value="{!ref.Rtf__c}" styleClass="ckeditor" richText="false"/>

The purpose of the menu is to allow fragments of text to be inserted when selected via the context menu. (The actual JavaScript menu code is much longer and generated by Apex code using data returned by describe calls; it is also loaded from a static resource so that it can be cached by the browser.)

Here is a screen shot from the code posted here:

Menu (1)

A brief explanation of the code is that the menu items either return other menu items (when they are the root menu or a sub-menu) or reference a command (when they are a leaf action). The menu groups add dividing lines into the menus. The editor is added based on the ckeditor class name.

The code:

<script src="//cdn.ckeditor.com/4.5.9/standard/ckeditor.js"></script>
<script>

// Menu code
function addMergeFieldMenu(event) {
    var e = event.editor;
    
    // Commands
    e.addCommand("cv_claimant_Birthdate", {
        exec: function(e) {
            e.insertText("\{\!claimant.Birthdate\}");
        }
    });
    e.addCommand("cv_claimant_Name", {
        exec: function(e) {
            e.insertText("\{\!claimant.Name\}");
        }
    });
    e.addCommand("cv_claim_Name", {
        exec: function(e) {
            e.insertText("\{\!claim.Name\}");
        }
    });
    e.addCommand("cv_claim_CreatedDate", {
        exec: function(e) {
            e.insertText("\{\!claim.CreatedDate\}");
        }
    });
    
    // Listener
    e.contextMenu.addListener(function(element, selection) {
        return {
            cv: CKEDITOR.TRISTATE_ON
        };
    });
    
    // Menu Groups; different numbers needed for the group separator to show
    e.addMenuGroup("cv", 500);
    e.addMenuGroup("cv_people", 100);
    e.addMenuGroup("cv_claim", 200);
    
    // Menus (nested)
    e.addMenuItems({
        // Top level
        cv: {
            label: "Insert Merge Field",
            group: "cv",
            getItems: function() {
                return {
                    cv_claimant: CKEDITOR.TRISTATE_OFF,
                    cv_claim: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // One sub-menu
        cv_claimant: {
            label: "Claimant Person (claimant)",
            group: "cv_people",
            getItems: function() {
                return {
                    cv_claimant_Birthdate: CKEDITOR.TRISTATE_OFF,
                    cv_claimant_Name: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // These run commands
        cv_claimant_Birthdate: {
            label: "Birthdate (Birthdate: date)",
            group: "cv_people",
            command: "cv_claimant_Birthdate"
        },
        cv_claimant_Name: {
            label: "Full Name (Name: string)",
            group: "cv_people",
            command: "cv_claimant_Name"
        },
        // Another sub-menu
        cv_claim: {
            label: "Claim (claim)",
            group: "cv_claim",
            getItems: function() {
                return {
                    cv_claim_CreatedDate: CKEDITOR.TRISTATE_OFF,
                    cv_claim_Name: CKEDITOR.TRISTATE_OFF,
                };
            }
        },
        // These run commands
        cv_claim_Name: {
            label: "Claim Number (Name: string)",
            group: "cv_claim",
            command: "cv_claim_Name"
        },
        cv_claim_CreatedDate: {
            label: "Created Date (CreatedDate: datetime)",
            group: "cv_claim",
            command: "cv_claim_CreatedDate"
        },
    });
}

// Add and configure the editor
function addEditor() {
    CKEDITOR.replaceAll('ckeditor');
    for (var name in CKEDITOR.instances) {
        var e = CKEDITOR.instances[name];
        e.config.height = 400;
    }
    CKEDITOR.on('instanceReady', addMergeFieldMenu);
}
addEditor();

</script>

Making one DataTable respond to search/length/page changes in another DataTable

DataTables can be applied to multiple tables in a page and sometimes the content in one table needs to be driven by the content in another table. For example, checkboxes in the first table on a page might act as a filter on later tables in a page. (The setup of such filtering is not covered here; the mechanism that can be used is custom filtering.)

But DataTables also supports a search mechanism that reduces the table rows to ones that match and a pagination mechanism where the number of rows shown or the page shown can be changed. By default, changes to those values in the first table will not cause the later tables to be re-filtered and re-drawn. So if you want the later tables to only correspond to the checked checkboxes that are visible in the first table, extra code has to be added.

The good news is that DataTables does generate events for the changes and so provides a convenient point to hook in extra code. The only complication is that it appears necessary to defer until after the current event processing has been completed by using setTimeout.

So assuming the tables are distinguished by classes firstMarker and laterMarker, this code will get the later tables re-drawn (and so re-filtered) to be consistent with the first table:

(function($) {
    $(document).ready(function() {

        var firstTable = $('table.firstMarker');
        firstTable.DataTable();

        var laterTables = $('table.laterMarker');
        var laterDataTables = [];
        laterTables.each(function() {
            laterDataTables.push($(this).DataTable());
        });

        var drawLaterDataTables = function() {
            setTimeout(function() {
                $.each(laterDataTables, function(index, value) {
                    value.draw();
                });
            }, 0);
        };
        firstTable.on('search.dt', drawLaterDataTables);
        firstTable.on('length.dt', drawLaterDataTables);
        firstTable.on('page.dt', drawLaterDataTables);

        // Filtering logic not shown here
    });
})(jQuery.noConflict());

My week with Heroku Connect

Heroku Connect replicates and synchronises data between a Salesforce org and a Heroku Postgres database. So you can build an app in Heroku using one of seven technologies (including Node.js), hook that app up to the Postgres database, and then your app users will be able to see and modify the same data that the Salesforce users see and modify. Bear in mind that this mechanism allows you to share data: it does not allow you to share business logic.

As this solution is built in/on Heroku, it is incredibly easy to setup and get working; I had the Demo Edition setup and the first couple of SObjects working inside an hour. The mapping UI involves a lot of checkbox clicking, but once done the configuration can be exported and imported. The UI provides overview and drill-down on what is going on. One minor problem I had was quickly and efficiently sorted out by Heroku support. All good.

The clean and simple scenario is where the Postgres data is largely handled as read-only. Then (besides whatever technologies you are writing the Heroku app in) the main thing to get used to is that you are writing SQL not SOQL. Also there are two identifier values: the SFID which is the 18 character (case insensitive) Salesforce ID we are used to and a local Postgres integer ID. Foreign key fields from Salesforce reference the SFID not the ID.

Things get more awkward when you want to insert or update the Postgres data. Heroku Connect handles the synchronisation in that direction too, but some aspects of the implementation leak out:

  • The SFID is not available in the transaction where an insert is done (as it is allocated in Salesforce). In my very limited tests, it was available about a second later.
  • Fields such as CreatedDate and any fields populated by Salesforce logic such as insert or update triggers will remain empty until the next synchronization is done; in the basic polling mode that can be up to 10 minutes later. So your users may experience blank fields in the UI when they insert data that change to populated fields when the UI is refreshed 10 minutes later.

Inserting objects into Postgres that have parent/child relationships is awkward. For master/detail, the required pattern is documented in Inserting records with master/detail relationships using external IDs. It requires an “External ID” field to be added to the Salesforce objects (so is a little intrusive at that side) and the Heroku code needs to populate that field and (an automatically added) foreign key with a matching unique value. This then allows Salesforce to create the same master/detail relationship using the normal Salesforce identifier. I don’t know if, but do hope that, the Salesforce foreign key is pushed back into Postgres fairly immediately like the SFID is. There is no mention of how to accomplish this for lookup relationships.

I must mention the pleasure and productivity of writing the full application stack in one language and in JavaScript: AngularJS for the client and Node.js/Express/pg-promise/Passport for the server. All quick to get running for existing Salesforce data thanks to Heroku and Heroku Connect.

Adding JavaScript and jQuery to Visualforce pages

Visualforce is pretty tolerant of how custom JavaScript is added, but this post suggests a couple of patterns to use (where possible):

  • External JavaScript file references or local JavaScript is best placed at the end of the page rather than the beginning which looks a bit strange at first sight. The benefit is that the page content can then be rendered by the browser before the browser becomes blocked loading the external JavaScript file and then executing that file and the local Javascript. So a potential page load delay is avoided, with the JavaScript work being completed in the user’s “thinking time” as they first see the page content.
  • It is easy to unintentionally add references to Javascript’s global scope and so potentially interfere with other references in that scope. In JavaScript scope is delineated by functions (not by blocks – curly brackets have no impact on scope) so var declarations should always be used and should always be used within a function.

So when not using external libraries put this executed (via the ()) anonymous JavaScript function (to create a new scope) at the end of the page:

<apex:page ...>
    <apex:sectionHeader .../>
    <apex:pageBlock ...>
        ...
    </apex:pageBlock>

<script>
(function() {
    // All the custom JavaScript goes in here
    // Always use var
    var i = ...;
    ...
})();
</script>
</apex:page>

and when using jQuery put both the include and the executed anonymous JavaScript function at the end of the page:

<apex:page ...>
    <apex:sectionHeader .../>
    <apex:pageBlock ...>
        ...
    </apex:pageBlock>

<apex:includeScript value="{!URLFOR($Resource.jQueryZip, 'jquery.js')}"/>
<script>
(function($) {
    // All the custom JavaScript goes in here
    // Always use var
    var i = ...;
    // Use $ in here for jQuery
    var j = $('table.notes');
    ...
})(jQuery.noConflict());
</script>
</apex:page>

This allows $ to be used as the reference to jQuery within the function, while ensuring that whatever the symbol $ was set to before jQuery was included is restored via the noConflict call.

Creating a custom global describe API using @RestResource

A colleague is working on a client that needs to know all the SObject names and all the field names within those SObjects. The Apex describe APIs provide this information but also a lot of other information that is not required in this case. So it is worth doing work at the server-side to cut down the information to only what is required by the client.

(In the org in question, the 300 SObjects produce JSON output of 800 kB, well below the 3 MB governor limit on HTTP responses.)

Salesforce’s @RestResource mechanism makes doing this pretty easy. The code below transfers the required information into instances of simple Apex classes, sorts the data based on label first then API name second, and then leaves it up to the platform to serialise those as JSON:

@RestResource(urlMapping='/v1/describe')
global with sharing class DescribeRest {

    global class Sob implements Comparable {
        
        public String sobLabel;
        public String sobApi;
        public Field[] sobFields;
        
        Sob(SObjectType t) {
            DescribeSObjectResult r = t.getDescribe();
            sobLabel = r.getLabel();
            sobApi = r.getName();
            sobFields = new Field[] {};
            for (SObjectField f : r.fields.getMap().values()) {
                sobFields.add(new Field(f));
            }
            sobFields.sort();
        }
        
        public Integer compareTo(Object o) {
            Sob that = (Sob) o;
            if (this.sobLabel < that.sobLabel) return -1;
            else if (this.sobLabel > that.sobLabel) return 1;
            else {
                if (this.sobApi < that.sobApi) return -1;
                else if (this.sobApi > that.sobApi) return 1;
                else return 0;
            }
        }
    }
    
    global class Field implements Comparable {
        
        public String label;
        public String api;
        
        Field(SObjectField f) {
            DescribeFieldResult r = f.getDescribe();
            label = r.getLabel();
            api = r.getName();
        }
        
        public Integer compareTo(Object o) {
            Field that = (Field) o;
            if (this.label < that.label) return -1;
            else if (this.label > that.label) return 1;
            else {
                if (this.api < that.api) return -1;
                else if (this.api > that.api) return 1;
                else return 0;
            }
        }
    }
    
    @HttpGet
    global static Sob[] get() {
        
        Sob[] sobs = new Sob[] {};
        for (SObjectType t : Schema.GetGlobalDescribe().values()) {
            sobs.add(new Sob(t));
        }
        sobs.sort();
        
        return sobs;
    }
}

When accessed using /services/apexrest/cveep/v1/describe.json, this produces JSON (formatted here to better illustrate the structure) taking about 10ms per object at the server-side:

[
    {
        "sobLabel":"Absence",
        "sobFields":[
            {"label":"Absence","api":"Name"},
            {"label":"Absence Type","api":"Type__c"},
            {"label":"Claim","api":"Claim__c"},
            ...
        ],
        "sobApi":"Absence__c"
    },
    ....
]

Now that the governor limits have been removed on describe calls the first limit that will be hit is probably the 3 MB response size limit.

Cool data tables using @RestResource, AngularJS and trNgGrid

I have an AngularJS application that shows tables of data using:

  • an Apex class that does dynamic SOQL and populates instances of a simple Apex class that are serialised to the client as JSON via the @RestResource annotation
  • the client side is AngularJS that pretty much just passes the JSON data through to a page template
  • the presentation work is all done by the excellent trNgGrid component and Bootstrap styling

The Apex code is clean and simple:

@RestResource(urlMapping='/report')
global without sharing class ReportRest {
    global class Claim {
        public String employeeName;
        public String department;
        public String reportsTo;
        public String claimNumber;
        public String status;
        public String leaveType;
        public Date startDate;
        public Date endDate;
        Claim(SObject c) {
            SObject e = c.getSObject('Employee__r');
            employeeName = (String) e.get('Name');
            department = (String) e.get('Department');
            SObject r = e.getSObject('ReportsTo');
            reportsTo = r != null ? (String) r.get('Name') : null;     
            claimNumber = (String) c.get('Name');
            status = (String) c.get('Status__c');
            leaveType = (String) c.get('LeaveType__c');
            startDate = (Date) c.get('StartDate__c');
            endDate = (Date) c.get('EndDate__c');
        }
    }
    @HttpGet  
    global static Claim[] get() {
        Claim[] claims = new Claim[] {};
        String soql = ...;
        for (SObject sob : Database.query(soql)) {
            claims.add(new Claim(sob));
        }
        return claims;
    }
}

and the trNgGrid markup is even more impressive:

<table tr-ng-grid="tr-ng-grid" class="table table-condensed" items="items"
      order-by="orderBy" order-by-reverse="orderByReverse">
  <thead>
    <tr>
      <th field-name="employeeName"/>
      <th field-name="department"/>
      <th field-name="reportsTo"/>
      <th field-name="claimNumber"/>
      <th field-name="status"/>
      <th field-name="leaveType"/>
      <th field-name="startDate" display-format="longDate" display-align="right"/>
      <th field-name="endDate" display-format="longDate" display-align="right"/>
    </tr>
  </thead>
</table>

You just define the column headers and trNgGrid generates the rows from the JSON data array (called “items” here). The resulting table has column sorting and column searching and other features can be enabled too. As it is written in AngularJS, it leverages AngularJS features such as filters (“longDate” here) for custom formatting.

What is great about this arrangement is that there is no tedious coding involved: all the code serves a purpose and the grunt work is handled by the frameworks. It also scores high on “ease of modification”: an extra column only takes a few minutes to add.

(Contrast this with the JavaScript required in e.g. Connecting DataTables to JSON generated by Apex.)

Here is a screen shot from the real application (with different columns):

TrNgGrid