Table Component: Custom Views

A View controls the appearance of a Table Component. It supplies the functions necessary to render the component.

Skuid provides two table views:

  • Standard, which looks and acts like a powerful table component, with inline editing, etc.
This view will display all selected field columns, along with any number of loaded rows. In this example, our data set will be a list of products. Each product will have values in a Product Code, Product Name, Product Family, and Product Description column. Most of the products we will be working with are in the "shoe" product family.
  • Photo, which renders a list of images with an optional caption
Instead of showing large amounts of data at a glance, the photo view focuses more on visual presentation with limited data. In this example, the end user would see photos of the individual products—again mostly shoes.

Together, these options cover most common use cases, but if you have more specific needs then a Custom View may provide an easier alternative to creating a completely custom component.

Some of the advantages of a Custom Table View over a Custom Component:

  • You don’t have to create a Builder to be able to set common properties from the Page Composer.
  • Your users can switch back and forth between the table view and the Custom View (if you allow it), or between multiple Custom Views.
  • You get Pagination (including the “Load More” feature).
  • You get Filters, Mass Actions and Global Actions.
  • You get Save/Cancel buttons and the Search Bar.
  • You get the power and feature-set of the skuid.ui.Item and skuid.ui.List objects.

Custom View Reference

A Custom View snippet runs when the page loads.

A Custom View snippet does not receive any parameters. Instead, it must return an object which defines certain functions. Initialization, rendering of particular Items and cleanup are handled in these functions.

A Custom View Snippet must return an object with the following:

PROPERTIES

  • alwaysFullRender (boolean): Optional. A value indicating if Items should always be re-rendered. Defaults to false, allowing Items to be cached. Set this value to true to prevent caching. Note that this may result in decreased performance.

FUNCTIONS

  • setUp (function): Optional. Called whenever the the View is first rendered. For example, when the page first loads, or after switching from one view to another.
    Function Parameters:
    • – list(skuid.ui.List): Used to control and get information about the current Table.
  • beforeRender (function): Optional. Called when the page first loads, after a pagination event, when edited/deleted records are saved/canceled and when a new record is added to the table. beforeRender is executed just before the currently visible Items are detached from the DOM (if there are any) and the next group of Items are rendered.
    Keep in mind that, because this function is called before the prior group of Items have been removed, some of the list’s variables—such as visibleItems and contents—represent the “prior” state of the table. Thus, adding elements to the contents element will have no visible effect (as it will be cleared immediately after this function) and pulling Items from visibleItems may produce unexpected results (or errors, since visibleItems is undefined when first rendering the Table). When implementing beforeRender, it’s recommended that you avoid modifying the contents of the Table.
    Function Parameters:
    • – list (skuid.ui.List)**: Used to control and get information about the current Table.
  • render (function): Required. This function is responsible for rendering the given Item. It is called the first time the Item appears within the Table and, if alwaysFullRender is false, is not called again unless the View or Page is reloaded.
    Function Parameters:
    • – item (skuid.ui.Item): Used to get information about the current Item within the Table.
  • renderComplete (function): Optional. Called after all visible Items have been rendered and the Items have been appended to the Table.
    Function Parameters:
    • – list (skuid.ui.List): Used to control and get information about the current Table.
  • tearDown (function): Optional. Called just before switching to another View. This function provides an opportunity to handle any last-minute cleanup tasks necessary to free up memory, etc. This function is notcalled when the window is closed or the page is refreshed.
    Function Parameters:
    • – list (skuid.ui.List): Used to control and get information about the current Table.

IMPORTANT NOTE: “Rendering” here refers to generating HTML elements. Due to various optimizations within Skuid, there is no guarantee at any point that List or Item elements have been rendered on the screen or even added to the DOM. Some element attributes, such as height and width, may or may not be completely initialized. When building a Custom View, it is best to use standard flow layouts rather than depending on dynamic layout using absolute positioning, etc.

Example 1: An Empty Shell

Custom Views are more complicated than most snippets. This example demonstrates how to build a minimally functional Custom View which can then be used as a template for more advanced Custom Views.

To begin, create a new Skuid page with the following properties:

  • Page Name: SkuidSample_CustomView
  • Type: Desktop
  • Template: Object list / tab page
  • Select Primary Object: Product2

On the new page, ensure that the “Name” field is included in the Products2 Model.

Select the Table component and click “Add Features” then “Add View.”

This will add a new View labeled “Standard List View” (this is a default value). Click the View and change the following properties:

  • View Type: Custom
  • View Label: Basic View Template
  • View Icon: ui-silk-wrench
  • Snippet Name: SkuidSample.EmptyView.png4

Open the “Resources” tab and create a new Javascript resource with the following properties:

  • Resource Location: In-Line (Snippet)
  • Snippet Name: SkuidSample.EmptyView

The code for this example is little more than a shell. It demonstrates one strategy for building the return object which Skuid, in turn, uses to build the Table View.

Using this strategy, each of the functions are defined individually and given names that are meaningful to their context. Shared variables are easily declared and used between them. An object can then be created which references the functions and associates them with names that Skuid will recognize.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Declare variables that are shared by all of the functions

var closedDoorCount = 0;

// Declare your functions

function turnOnTheLights( list )
{
}

function answerTheDoor( list )
{
}

function welcomeEachGuest( item )
{
    item.element.text( item.row.Name );
}

function closeTheDoor( list )
{
    closedDoorCount += 1;
}

function turnOffTheLights( list )
{
    console.log( 'closeTheDoor was called ' + closedDoorCount + ' times.' );
}

// Finally, package the functions into something that Skuid can call...

var customView =
    {
        /\* Required \*/ render: welcomeEachGuest,
        /\* Optional \*/ setUp: turnOnTheLights,
        /\* Optional \*/ beforeRender: answerTheDoor,
        /\* Optional \*/ renderComplete: closeTheDoor,
        /\* Optional \*/ tearDown: turnOffTheLights
    };
return customView;

The result of this snippet is unimpressive, but forms the basis on which we can build more complicated views.

At the moment, only an unstyled list of product name values from our example data set will be displayed.

Example 2: Rendering Complex Items

Building off the template from the previous example, we’ll now expand the rendering of Items into something a bit more useful.

In this example we’ll create a “Card View.” This view is similar in concept to the standard Photo View, but with much more data on the screen. We’ll also enable editing of the record directly from each card.

To begin, add the following fields to the Product2 Model from Example 1:

  • Product Id
  • Product Description
  • Product Family
  • Product Code
  • Image > AttachmentId (Image__r.skuid__AttachmentId__c)

If you can’t find the Image field, you may need to add it to your Product2 object. See Add Photo View to a Table.

Add a Condition to the Model with the following properties:

  • Field: Family
  • Operator: =
  • Value Content: Single specified value
  • Value: (blank)
  • State: Filterable Default Off
  • Condition Name: Family

Add a “Table Filter” to the Table with the following properties:

  • Filter Type: Select Option
  • Model Condition to Affect: Family
  • Create a “Filter Off” Option: checked
  • (…Add a Condition Source…)
  • Source Type: Picklist Options for Condition’s Field

Create a new View with the following properties:

  • View Type: Custom
  • View Label: Cards
  • View Icon: ui-silk-vcard
  • Snippet Name: SkuidSample.CardView

Create a new snippet with the following properties:

  • Resource Location: In-Line (Snippet)
  • Snippet Name: SkuidSample.CardView

To make the jQuery “$” shortcut available to all functions, we’ll declare a single view-scoped variable at the top of the snippet:

1
var $ = skuid.$;

In the Standard View, the “Add Row” button is located in the table header. The table header is not rendered in a Custom View, however, so we’ll need to create an “Add Product” button during setup, as encapsulated within the “createAddButton” function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function createAddButton( list )
{
    var addButton = $( '<div>' )
        .addClass( 'customview-button' )
        .on( 'click', function() { list.model.createRow(); })
        // Add the "plus" icon
        .append( $( '<div>' ).addClass( 'ui-silk ui-silk-add' ) )
        // Add the text
        .append( $( '<div>' ).text( 'Add Product' ) );

    // Insert the "Add" button before the list contents
    list.contents.before( addButton );
}

For this example, though, most of the code will focus on rendering Items as cards.

Each card will have editable fields which can be activated using an “edit” button (just like they can with the row action button). To make the code a bit more succinct, add a helper function for creating fields:

function createEditableField( fieldId, model, row, item )
{
    var editor = new skuid.ui.Editor( $( '<div>' ) );
    editor.registerModel( model );

    var fieldOptions = { fieldId: fieldId, register: true, mode: item.mode };
    var field = new skuid.ui.Field( row, model, editor, fieldOptions);
    item.registerField( field );
    field.render();

    return field;
}

This function creates a skuid.ui.Editor, wraps it in a skuid.ui.Field and then registers it with a skuid.ui.Item. The advantage of creating a Field (over trying to implement everything yourself) is that a lot of the plumbing involved in wiring the editor up to the Model is handled for you. When you enable editing of the Item (which can be done with one line of code), it automatically enables editing for all registered Fields. The Fields allow direct editing of the given skuid.model.Model. When the Model data is changed, the Table is automatically notified and, among other things, the Save/Cancel buttons are enabled. It is significantly easier to use a Skuid Model than it is to try to handle all of this complexity on your own.

The majority of the code in this example is in the render function, named renderCard. It creates the editable fields and HTML elements for the the cards.

function renderCard( item )
{
    var row = item.row,
        model = item.list.model;

    var isNew = skuid.model.isNewId( row.Id );

    // set the Item's mode to "edit" for new rows. This will cause the Item's
    // field's to be automatically rendered in edit mode.
    if ( isNew )
        item.mode = 'edit';

    // create a wrapper function to make creating fields a bit less verbose
    var createField =
        function( fieldId )
        {
            return createEditableField( fieldId, model, row, item );
        };

    // create the editable fields
    var fName = createField( 'Name' ),
        fCode = createField( 'ProductCode' ),
        fFamily = createField( 'Family' ),
        fDesc = createField( 'Description' );

    var card = item.element
        .addClass( 'customview-card' );

    // header
    var header = $( '<div>' )
        .addClass( 'customview-header' )
        .appendTo( card );

    // card delete button
    $('<div>')
        .addClass( 'ui-silk ui-silk-delete' )
        .addClass( 'customview-cardbutton' )
        .on( 'click', function() { item.toggleDelete( true ); })
        .appendTo( header );

    // card edit button
    $('<div>')
        .addClass( 'ui-silk ui-silk-pencil' )
        .addClass( 'customview-cardbutton' )
        .on( 'click', function() { item.toggleEdit(); })
        .appendTo( header );

    // product name
    fName.element
        .addClass( 'customview-content' )
        .appendTo( header );

    var content = $( '<div>' )
        .appendTo( card );

    // image
    $( '<img>' )
        .attr( 'src',
            row.Image__r ?
                skuid.utils.getUrlFromAttachmentId(
                    row.Image__r.skuid__AttachmentId__c ) :
                $( '#nx-images .default_org' ).text()
        )
        .appendTo(
            $( '<a>' )
                .addClass( 'customview-imageframe' )
                .appendTo( content )
                .attr( 'href', '/' + row.Id )
        );

    // properties
    $( '<div>' )
        .addClass( 'customview-details' )
        .append([
            $( '<div>' ).addClass( 'customview-label' )
            .text( 'Product Code' ),
            fCode.element,
            $( '<div>' ).addClass( 'customview-label' )
            .text( 'Product Family' ),
            fFamily.element,
            $( '<div>' ).addClass( 'customview-label' )
            .text( 'Description' ),
            fDesc.element
        ])
        .appendTo( content );

    // clear fix for floated elements
    $( '<div>' ).css( 'clear', 'both' ).appendTo( content );
}

As you may have noticed, the styling for the card is expressed via CSS classes (added to our elements via jQuery’s addClass function). For most snippet examples, the styling is relatively minor. For this example, however, there was a great deal of styling to be done. To prevent the snippet from becoming unwieldy, we’ll place the style information for the elements in a CSS resource.

Open the “Resources” tab and create a new CSS resource with the following properties:

  • Resource Location: In-Line
  • Snippet Name: Card Styling

Insert the following CSS code into the Resource Body:

.customview-button
{
    color: white;
    background-color: #006666;
    border-radius: 4px;
    font-weight: bold;
    font-size: 8pt;
    margin: 3px 6px 8px 0px;
    padding: 4px 8px 5px 8px;
    white-space: nowrap;
    position: relative;
    cursor: pointer;
    opacity: 1;
    box-shadow: 0px 0px 6px 2px #DDD;
    display: inline-block;
}
.customview-button DIV
{
    display: inline-block;
    vertical-align: middle;
}
.customview-button DIV:last-child
{
    margin: 0pt 4pt;
}
.customview-card
{
    display: inline-block;
    width: 450pt;
    background-color: #FFF;
    border: 1px solid #299fc5;
    border-radius: 4pt;
    overflow: hidden;
    margin: 4pt;
    vertical-align: top;
}
.customview-card .customview-header
{
    border: 1px solid #299fc5;
    background-color: #299fc5;
    color: #fff;
    font-weight: bold;
    padding: 2pt 8pt;
}
.customview-card.deleted
{
    border: 1px solid red;
    background-color: rgb(255,230,230);
}
.customview-card.deleted .customview-header
{
    border: 1px solid red;
    background-color: red;
}
.customview-header A,
.customview-header A:hover
{
    color: #FFF;
    text-shadow: 1pt 1pt 0 #000;
    font-size: 16pt;
}
.customview-header .nx-modified A,
.customview-header .nx-modified A:hover
{
    color: #bba055;
}
.customview-header .customview-content
{
    width: 75%;
}
.customview-header .customview-cardbutton
{
    float: right;
    margin: 4pt;
    cursor: pointer;
}
.customview-imageframe
{
    display: block;
    background-color : #299fc5;
    float: right;
    max-width: 192pt;
    max-height: 144pt;
    margin: 2pt;
}
.customview-imageframe IMG
{
    max-width: 184pt;
    max-height: 136pt;
    display: block;
    border-radius: 16pt;
    border: 2pt solid #299fc5;
}
.customview-details
{
    padding: 0pt 6pt 10pt 10pt;
    overflow: hidden;
}
.customview-details .customview-label
{
    font-weight: bold;
    margin-top: 8pt;
}

For this example, we do not need to provide the “before render,” “render complete” and “tear down” functions. All that’s left is to bundle the two functions we are going to use into an object and then return it to Skuid:

1
2
3
4
return {
        setUp: createAddButton,
        render: renderCard
    };

Now Preview the page and select “Cards” from the Views list.

Rather than seeing the standard table, you can now view and edit your data as cards. Notice that filters and pagination still work. We didn’t have to do anything special for that!

Using this custom view, all records will display as cards. In the example described here, the Add Product button will create a new product record—displayed as a card. Records within cards can be edited using the edit icon within them, and they can be marked for deletion by clicking the delete icon within them. The card view, since it is simply displaying records in a different way, will also be affected by filters. In this example, we can filter the data to only display records within the "Shoe" product family, and the card view will adjust accordingly.