Dan Shahin
Apr 7, 2014

BadBearUI 

The Force.com platform offers a lot of great tools for quickly creating a data model and adding a basic user interface on top.  But sometimes our clients complain that it takes "too many clicks" to get certain tasks done, and some built-in UI elements are cumbersome and often confusing to new users. In this blog, we'll see how to use Javascript Remoting to make pages more responsive and usable. Take the standard multi-select picklist as an example.  Users sometimes struggle with control-clicking multiple elements, and if you have a large list of options, it can be a pain to scroll through them. In our hypothetical use case, we are writing a data entry app for describing a group of bears.  Each bear is a record with a self-referring lookup to a single parent bear.  While perhaps not zoologically correct, this is a common data model for managing hierarchies of records.  We will just pretend that our client did not have the budget for two parent bear fields in this phase of the project. This animated gif shows the process of navigating to a record and updating a multi-select picklist of adjectives to describe the record : BadBearUI Not too bad, but notice that the user struggles a bit to select just the right adjectives from an unwieldy list in a popup before having to click save.   That may not seem like a lot of work, but if it's your job to describe bears all day long, you'll end up doing this routine countless times a day.  We can do better. Wouldn't it be great to have a user experience that looks more like this? ButtonlessBearUI Now we can navigate the hierarchy of bears quickly and easily add or remove adjectives in a less cluttered interface.  We saved a lot of clicks and gained a better user experience. For the purposes of this demo, we use Toastr.js to give a visual indicator that your record is saved every time you add or remove a value from the picklist.  The actions only send snippets of JSON back and forth instead of the entire view state like traditional Visualforce action methods. You'll notice the use of JQuery-UI Autocomplete to select the record and the excellent Chosen.js is used for the picklist.   We've also added simple user notification using Toastr.js, and the whole thing is rendered dynamically using Handlebars.js templates. All of this functionality is driven by one of the best additions to the Force.com platform in years:  Javascript Remoting, also known as JSR. You can browse the Buttonless Bear Browser code below. The Javascript and CSS are saved as static resources loaded by the page, so that they can be cached by the browser. It also helps to load the Javascript this way because you will see the correct line numbers in any js console messages or errors.  Line numbers that would otherwise seem incorrect because of the many lines of html that are inserted into a Visualforce page.

<apex:page showHeader="true" sidebar="false" doctype="HTML-5.0" controller="JSRDemoController">
<head>
<apex:stylesheet value="{!URLFOR($Resource.BearsCSS)}"/>
<apex:stylesheet value="{!URLFOR($Resource.JQuery_UI_1_8_16_custom, 'css/smoothness/jquery-ui-1.8.16.custom.css')}"/>
<apex:stylesheet value="https://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"/>
<apex:stylesheet value="//cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/css/toastr.min.css"/>
<apex:stylesheet value="//cdnjs.cloudflare.com/ajax/libs/chosen/1.1.0/chosen.css"/>
</head>
<body>
<h1>Buttonless Bear Browser</h1> <br/>
<input id="bearName" placeholder="Bear Name..."/>
<div id="main"/>
<hr/>
© 2014 Bears Across America
<!-- handlebars.js template -->
<script id="bear-family-template" type="text/x-handlebars-template">
<input id="bearId" type="hidden" value="{{Id}}"/>
<div id="lineage">
Parent:<span id="{{Parent__c}}" class="parent">{{Parent__r.Name}}</span><br/>
</div>
<select id="adjectives" data-placeholder="Bear Descriptors..." multiple="multiple">
<apex:repeat value="{!adjectives}" var="adj">
<option class="adj" value="{!adj}">{!adj}</option>
</apex:repeat>
</select>
<p/>
<h2>children</h2>
<ul id="sortable">
{{#each JSRObject__r}}
<li id="{{this.Id}}" class="child">{{this.Name}}{{this.Order__c}}</li>
{{/each}}
</ul>
</script>
<!-- javascript resources should be loaded last -->
<script src="https://code.jquery.com/jquery-2.1.0.min.js"/>
<script src="https://code.jquery.com/ui/1.10.3/jquery-ui.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/js/toastr.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0-alpha.2/handlebars.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/chosen/1.1.0/chosen.jquery.min.js"/>
<script src="{!URLFOR($Resource.BearBrowser)}"/>
</body>
</apex:page>
$(document).ready(function(){
toastr.options = {
"closeButton": true,
"debug": false,
"positionClass": "toast-bottom-full-width",
"onclick": null,
"showDuration": "300",
"hideDuration": "1000",
"timeOut": "5000",
"extendedTimeOut": "1000",
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut"
}
toastr.info('Bears are mammals.', 'Toastr info');
toastr.warning('Bears are dangerous.', 'Toastr warning');
toastr.error('Bears are attacking me!', 'Toastr error');
//setup vars
var source = $("#bear-family-template").html(),
template = Handlebars.compile(source),
context = {
JSRObject__r: []
},
//merges a handlebars.js template with a json context
//and displays them in the #main div
renderPage = function(context){
var html = template(context);
$('#main').html(html);
},
//a wrapper function around a JSRemoting method
//that takes the results and renders a handlebars.js
//template with the JSON the method returns
fetchFamily = function(parentId){
JSRDemoController.fetchFamily(parentId,function(result,event){
if(event.status){
var context = result,
adjectives = [];
if(result.Adjectives__c !== undefined){
adjectives = result.Adjectives__c.split(';');
}
renderPage(context);
//make sure to select all of the previously
//chosen adjectives for this bear
$('option.adj').each(function(){
var $opt = $(this);
for(var i=0; i<adjectives.length; i++){
var adj = adjectives[i];
if($opt.val() == adj){
$opt.attr({'selected' : 'selected'});
}
}
});
//initialize chosen.js to display the current
//bear adjectives
$('#adjectives').chosen({
no_results_text: "Oops, nothing found!",
width : "350px"
}).change(function(){
var adjectives = $(this).val() || [],
bearId = $('#bearId').val(),
bearName = $('#bearName').val();
//now call another JSR method inside an event handler
JSRDemoController.updateAdjectives(bearId, adjectives,function(result,event){
if(event.status && result){
toastr.success('updated ', bearName);
}else{
toastr.error(event.message);
}
});
});
}else{
toastr.error(event.message);
}
});
}
//setup event handlers
$('#bearName').autocomplete({
source:function(request, callback ){
//using JSR inside of an event handler
//to dynamically populate autocomplete values
JSRDemoController.bearNames(
request.term, //value that is typed in autocomplete
function(result,event){
if(event.status){
callback(result);
}else{
toastr.error(event.message);
}
}
);
},
select: function( event, ui ) {
var parentId = ui.item.name;
fetchFamily(parentId); //wrapped jsr method
}
});
//making sure that any new list elements rendered also
//get event handler attached
$('body').on('click', 'li.child,span.parent',function(){
var parentId = $(this).attr('id');
fetchFamily(parentId);//wrapped jsr method
$('#bearName').val($(this).text());
});
//on page load fire this raw JSR method
JSRDemoController.sayHello(function(result,event){
if(event.status){
toastr.success(result);
}else{
toastr.error(event.message);
}
});
});

Here is the controller, which uses Javascript Remoting methods to make the page snappy and responsive.

global with sharing class JSRDemoController {
    public String[] adjectives {get;set;}
    public JSRDemoController() {
        adjectives = new List<String>{};
        //populate the multi-select options from Metadata
        for(Schema.PicklistEntry ple : Schema.sObjectType.Bear__c.fields.Adjectives__c.getPicklistValues()){
            adjectives.add(ple.getLabel());
        }
    }
    @RemoteAction
    global static String sayHello() {
        return 'Hello ' + UserInfo.getName() + '. Ready to browse the bears.';
    }
    @RemoteAction
    global static bear[] bearNames(String nameFragment) {
        bear[] bearNames = new bear[]{};
        //nameFragment = nameFragment.replaceAll(/n/, '');
        nameFragment = ''%' + String.escapeSingleQuotes(nameFragment) + '%'';
        String soqlString = 'Select Id, Name, Adjectives__c from Bear__c where Name like ' + nameFragment;
        Bear__c[] results = Database.query(soqlString);
        for(Bear__c jsr: results){
            bearNames.add(
            new bear(jsr.id, jsr.Name, jsr.Name)
            );
        }
        return bearNames;
    }
    @RemoteAction
    global static Bear__c fetchFamily(String parentId) {
        Bear__c parent ;
        //query the parent with a sub query for the children
        parent = [Select Name,Id,Parent__c, Parent__r.Name, Adjectives__c,
        (Select Id,Name, Parent__c, Parent__r.Name from JSRObject__r)
        from Bear__c
        where id =: parentId];
        return parent;
    }
    @RemoteAction
    global static Boolean updateAdjectives(String bearId, String[] adjectives) {
        Bear__c bear = new Bear__c();
        bear.Id = bearId;
        bear.Adjectives__c = String.Join(adjectives,';');
        update bear;
        return true;
    }
    global class bear {
        public String name {get;set;}
        public String value {get;set;}
        public String label {get;set;}
        public bear(String name, String value, String label){
            this.name = name;
            this.value = value;
            this.label = label;
        }
    }
}
body {background-color: tan; font-family: sans-serif; font-size: 14px;}
span.parent:hover {cursor:pointer; background-color:pink;}
li.child{font-size: 24px;}
li.child:hover {cursor:pointer; background-color:pink; }
input#bearName {font-size: 24px; }
div#lineage {padding:4px;font-size: 20px;}

The CSS is loaded in the head portion of the page while Javascript is loaded last after all other page elements.  While this does not make your page download any faster, it does let it render progressively, which makes it seem faster to the end user. The Javascript resources are all minified versions loaded from external CDNs.  This is a great way to get started quickly developing with these libraries, but before deploying to production they should be saved as static resources.  A custom minified version of Jquery-UI should be created using the excellent download builder which will allow you to include only the components used by the page. Over my next several posts I will dig into the details of each Javascript library we have integrated, and how to reconcile them with the Force.com platform.

Accelerate your ISV Work.com strategy

Stay in the know on all things SaaS and Salesforce with The Decoder.

Join Our Newsletter

See what our newsletter has to offer:

Check out a recent copy here!

Join Our Mailing List