building a custom field renderer for multiselect picklists

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.skuid.com/t/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.

Peter, this is really cool.  Thanks for sharing! 

changed the touchstart event to tap instead to avoid problems with unintentionally tapping when swiping och touch devices.