Skip to content

Bootstrap Visual Timeline Component

Components are king!  Working on projects & applications, I often find myself digging though old code to find pieces to copy and reuse – especially when it comes to UI/UX.  About a year ago I started to build a library of VisualForce components that use javascript (jQuery) and Bootstrap after I discovered Visualstrap (VS) by Avinava Maiti.  He was able to build out a package with most of the Bootstrap 2 components in a Visualforce freindly fashion which makes building page with bootstrap streamlined and easy on the eyes from a coding perspective, here’s an example of implementing a panel with header (after implementing the 2 required VS components):

Bootstrap:

<div class="panel panel-default">
    <div class="panel-heading">
        <h3 class="panel-title">Panel title</h3>
    </div>
    <div class="panel-body">
        Panel content
    </div>
</div>

Visualstrap:

<vs:panel title="Panel Title" type="default">
    Panel Content
</vs:panel>

Both of these produce the same output, but with less coding under the Visualstrap component.
Screen Shot 2015-05-04 at 8.51.45 AM
Another advantage with using a VF component is the ability to target them with rendering actions from other form components.

The component I’m building is a visual timeline that can be used on any object with a date, and two string fields (title and description).  I found the CSS for this on bootdey.com, written by Deyson Bejarano.  I used one class as the controller, one component and one static resource for the CSS.  I have this demo setup as a page that’s uses Accounts as a standard controller, I’m displaying closed tasks for that account.  The timeline will look like this:

Screen Shot 2015-05-05 at 7.01.13 AM

Class:


global with sharing class bootTimeline {

global string queryString{get;set;}
global string dateString{get;set;}
global string titleString{get;set;}
global string descString{get;set;}

global map<string,list<timeline>> getLineMap(){
    map<string,list<timeline>> lineMap = new map<string,list<timeline>>();

    for(sObject s:database.query(queryString)){

        date thisDate = (date)s.get(dateString);

        timeline line = new timeline();
        line.lineMonth = monthYear(thisDate);
        line.lineDate = thisDate;
        line.dayName = dayOfWeek(thisDate);
        line.lineId = (string)s.get('id');
        line.lineTitle = (string)s.get(titleString);
        line.lineDesc = (string)s.get(descString);

        if(line.lineDesc.length() > 100)line.lineDesc = line.lineDesc.substring(0,100)+'...';

        list<timeline> lines = new list<timeline>();

        if(lineMap.get(line.lineMonth) != null)lines.addAll(lineMap.get(line.lineMonth));

        lines.add(line);

        lineMap.put(line.lineMonth,lines);

    }

    for(string s:lineMap.keySet()){

        list<timeline> lines = lineMap.get(s);
        lines.sort();
        lineMap.put(s,lines);

    }

    return lineMap;

}

global string monthYear(date thisDate){

    return monthMap.get(thisDate.month())+' '+thisDate.year();

}

global string dayOfWeek(date thisDate){

    date startOfWeek = thisDate.toStartOfWeek();

    integer daysApart = startOfWeek.daysBetween(thisDate);

    return dayShortMap.get(daysApart);

}

global final map<integer,string> monthMap = new map<integer,string>{
    1 => 'January',
    2 => 'February',
    3 => 'March',
    4 => 'April',
    5 => 'May',
    6 => 'June',
    7 => 'July',
    8 => 'August',
    9 => 'September',
    10 => 'October',
    11 => 'November',
    12 => 'December'
};

global final map<integer,string> dayShortMap = new map<integer,string>{
    0=>'Sun',
    1=>'Mon',
    2=>'Tue',
    3=>'Wed',
    4=>'Thu',
    5=>'Fri',
    6=>'Sat'
};

global class timeline implements Comparable{
    global string lineMonth{get;set;}
    global date lineDate{get;set;}
    global string dayName{get;set;}
    global string lineId{get;set;}
    global string lineTitle{get;set;}
    global string lineDesc{get;set;}

    global Integer compareTo(Object compareTo) {
        timeline compareToEmp = (timeline)compareTo;
        if (lineDate == compareToEmp.lineDate) return 0;
        if (lineDate > compareToEmp.lineDate) return -1;
        return 1;
    }
}

}

Component:

<apex:component controller="bootTimeline">

    <apex:attribute name="requireBS" type="boolean" default="true" description="Is bootstrap loaded already"></apex:attribute>
    <apex:attribute name="requireFA" type="boolean" default="true" description="Is font awesome loaded already"></apex:attribute>
    <apex:attribute name="query" type="string" required="true" assignTo="{!queryString}" description="Query for the timeline"></apex:attribute>
    <apex:attribute name="dateField" type="string" required="true" assignTo="{!dateString}" description="Date or DateTime field to base the timeline on"></apex:attribute>
    <apex:attribute name="titleField" type="string" required="true" assignTo="{!titleString}" description="Title field"></apex:attribute>
    <apex:attribute name="descField" type="string" required="true" assignTo="{!descString}" description="Description field"></apex:attribute>

    <apex:outputPanel rendered="{!requireBS}">
        <link href="https://netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"></link>
    </apex:outputPanel>
    <apex:outputPanel rendered="{!requireFA}">
        <link rel="stylesheet" type="text/css" href="https://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css"/>
    </apex:outputPanel>

    <link href="{!URLFOR($Resource.bootTimeline_css)}" rel="stylesheet"></link>

    <div class="container">
        <section id="news" class="white-bg padding-top-bottom">
            <div class="container">
                <div class="timeline">
                    <apex:repeat value="{!lineMap}" var="monthGroup">
                        <div class="date-title">
                            <span>{!monthGroup}</span>
                        </div>
                        <div class="row">
                            <apex:variable value="{!0}" var="counter"></apex:variable>
                            <apex:repeat value="{!lineMap[monthGroup]}" var="line">
                                <div class="col-sm-6 news-item {!IF(MOD(counter,2) == 0,'','right')}">
                                    <div class="news-content">
                                        <div class="date">
                                            <p>{!DAY(line.lineDate)}</p>
                                            <small>{!line.dayName}</small>
                                        </div>
                                        <h2 class="news-title">{!line.lineTitle}</h2>
                                        <p>{!line.lineDesc}</p>
                                        <a class="read-more" href="/{!line.lineId}" target="_blank">
                                            Read More
                                        </a>
                                    </div>
                                </div>
                                <apex:variable value="{!counter+1}" var="counter"></apex:variable>
                            </apex:repeat>
                        </div>
                    </apex:repeat>
                </div>
            </div>
        </section>
    </div> 

</apex:component>

The component has 6 attributes, 4 are required that control the data being displayed and the other 2 handle loading bootstrap css and font-awesome.

  • query: enter the query for the timeline to be run, you’ll need to include the date, title and description fields.  It’s also best to order this by the date field descending.
  • dateField: this is the api name of the date field
  • titleField: this is the api name of the title field, it can be any string field but should be one that is normally less than 80 characters like a standard name field
  • descField: this is the api name of the description field, if can be any string field.  The first 100 characters will display on screen.

Once the above are built, the timeline can be implemented on a VF page by the following line:


<c:bootTimeline
query="SELECT id, subject, description, activityDate FROM task WHERE accountId = '{!Account.Id}' AND isClosed = true ORDER BY activityDate desc"
dateField="activityDate"
titleField="subject"
descField="description">
</c:bootTimeline>

I’m not going to post the CSS as it’s to long, instead here is a link to it here. The final version can be previewed at https://bt-dashboard-developer-edition.na24.force.com/?id=0011a000003RIcV. Enjoy!

Published inApexBootstrapComponentsCSSVisualforce

Be First to Comment

Leave a Reply