building a custom field renderer for multiselect picklists

  • 1
  • Idea
  • Updated 4 years ago
  • Implemented
I had a requirement from a customer to build a mobile responsive Skuid page where multiselect picklists were rendered as lists with checkboxes instead of the regular on/off flipswitch. All options should be visible at all time since this is shown page that is scrolled top-down. Also the solution needed to work with condtional rendering so that when a picklist item is selected that immediately affected other dependent fields. After a few attempts to style this with pure CSS and a discussion with Rob https://community.skuidify.com/skuid/topics/checkbox-rendering-in-mobile I decided to go down the route to create custom field renderer instead. There are a couple of examples of this on the community but none that really came close to what I wanted. So I thought I'd share the solution I came up with in case anyone else needs something similar or maybe have suggestions for improvements. 
This is how I wanted the multiselect picklists to render

My solution uses a simple list styled with Font Awesome Icons to the left. Each list item (including the text as we wanted the entire line to be clickable) has an event handler that takes care of toggling selections and updating the model. The code for the field renderer is 
/* 
Renders a multiselect picklist as a list with simple checkboxes to the left using font awesome icons and 
classes. Both the checkbox and the label is clickable for each list item to invert the selection.
*/

var field = arguments[0],
    value = skuid.utils.decodeHTML(arguments[1]),
	$ = skuid.$;
	
var checked_iconClass = 'fa-check-square-o',                
    unchecked_iconClass = 'fa-square-o';
	
var duid = field.element[0].attributes.getNamedItem("data-uid").value,      // get data-uid for field
    picklistEntries = field.metadata.picklistEntries,                       // get list of picklist entries
    cellElem = field.element;                                               // get field element to append to 
    if(value !== null) {
        valueArray = value.split(';');                                      // split multiselect values into array by ;
    } else {
        valueArray = [];
    }

var plist = $( '<ul class="fa-ul">'  );                 // create the list container
$.each( picklistEntries, function( index, plvalue) {    // loop through list of all picklist entries 
    if($.inArray(plvalue.value, valueArray)>-1) {       // if picklist entry is in the value arrray
        icon = checked_iconClass;                       // set the icon to checked box
    } else {
        icon = unchecked_iconClass;                     // else set the icon to unchecked box
    }                                                   
    plist.append('<li><i class="fa-li fa ' + icon + '"></i>' +    
        plvalue.value + '</li></a>');                    // add a list entry for each picklist value
})

cellElem.append(plist);                                 // append list container to field element

if (field.mode == 'edit') {                             // attach click handler for edit mode
    $(document).on("click touchstart", "[data-uid=" + duid +"] li" , function() {               // handle clicks and taps on list items
        $(this).children('i').toggleClass(checked_iconClass).toggleClass(unchecked_iconClass);  // toggle checkbox
                                
        clickedText=this.lastChild.nodeValue;   // get text of clicked list item
        if(value !== null) {
            valueArray = value.split(';');      // split multiselect values into array by ;
        } else {
            valueArray = [];
        }
        
        valuePos = $.inArray(clickedText, valueArray);               // check if value exist in value array
        if(valuePos >-1) {                          // picklist value is selected so 
            valueArray.splice(valuePos,1);          // remove it from array
        } else {                                    // else value is not selected so
           valueArray.push(clickedText);            // add to array
        }
        if(valueArray.length >0) {
            valueStr = valueArray.join(';');
            field.model.updateRow(field.row, field.id, valueArray.join(';'),{ initiatorId: field._GUID });    // update model with array concat with ;
            value = valueStr;
        } else {
            console.log('empty array');
            field.model.updateRow(field.row, field.id, null,{ initiatorId: field._GUID });    // update model with array concat with ;
            value = null;
        }
    });
}
After doing this I also realized that for the page to look reasonable consistent visually I also needed to do something similar but a bit simpler for regular single select picklists that I wanted to look like:


The code for that renderer is:
/* 
Renders a picklist as a list with simple radio buttons to the left using font awesome icons and 
classes. Both the checkbox and the label is clickable for each list item to invert the selection.
*/

var field = arguments[0],
    value = skuid.utils.decodeHTML(arguments[1]),
	$ = skuid.$;
	
var checked_iconClass = 'fa-check-circle-o',                
    unchecked_iconClass = 'fa-circle-o';
	
var duid = field.element[0].attributes.getNamedItem("data-uid").value,      // get data-uid for field
    picklistEntries = field.metadata.picklistEntries,                       // get list of picklist entries
    cellElem = field.element;                                               // get field element to append to 
    
var plist = $( '<ul class="fa-ul">'  );                 // create the list container
$.each( picklistEntries, function( index, plvalue) {    // loop through list of all picklist entries 
    if(plvalue.value == value) {                        // if picklist entry is the selected value
        icon = checked_iconClass;                       // set the icon to checked box
    } else {
        icon = unchecked_iconClass;                     // else set the icon to unchecked box
    }                                                   
    plist.append('<li><i class="fa-li fa ' + icon + '"></i>' +    
        plvalue.value + '</li></a>');                    // add a list entry for each picklist value
})

cellElem.append(plist);                                 // append list container to field element

if (field.mode == 'edit') { // attach click handler for edit mode

    $(document).on("click touchstart", "[data-uid=" + duid +"] li" , function() {                                      // handle clicks on list items
        $(this).siblings().children('i').removeClass(checked_iconClass).addClass(unchecked_iconClass);      // turn off all sibling radioboxes
        $(this).children('i').addClass(checked_iconClass).removeClass(unchecked_iconClass);                 // set selected radio box
        clickedText=this.lastChild.nodeValue;                                                               // get text of clicked list item
        
        field.model.updateRow(field.row, field.id, clickedText,{ initiatorId: field._GUID });    // update model with clicked item ;
        value = clickedText;
        
        
    });
}

I've so far tested this with Firefox, Chrome and Safari for Mac and IE, Firefox and Chrome for Windows, and for Safari and Chrome for iPad. 

Javascript and CSS manipulation is not really my main area of development so suggestions for improvements are very welcome. I know that the array handling can be made more efficient in the first example but I preferred to keep the code more readable.
Photo of Peter Baeza

Peter Baeza

  • 2,868 Points 2k badge 2x thumb

Posted 4 years ago

  • 1
Photo of Rob Hatch

Rob Hatch, Official Rep

  • 44,006 Points 20k badge 2x thumb
Peter, this is really cool.  Thanks for sharing! 
Photo of Peter Baeza

Peter Baeza

  • 2,868 Points 2k badge 2x thumb
changed the touchstart event to tap instead to avoid problems with unintentionally tapping when swiping och touch devices.