Custom Renderer
With the custom renderer it is possible to visualise external data based on an HTML template. A custom version of the Bootstrap framework is included to easily build basic HTML templates.
Warning
At the moment there is no official support to add create, update and delete records. The same applies for list and item functions. The custom renderer is still in beta phase and is subjected to changes.
Configuration Script
Every multi record can be easily transformed into a beautiful HTML template by specifying the template in the configuration script of the multi record view. The concept is always as follows:
/**
* Declare a function in a new variable. This function needs to return the HTML as a string.
* Optional - Pass in three arguments: rowData, formattedData and csl.
* rowData gives you access to the current row. All values are invariant and in string format.
* formattedData gives you access to the current row. All values are formatted as configured in the view formatter.
* csl gives you access to the entire CSL library.
*/
const templateFunction = function(rowData, formattedData, csl) {
/**
* You get access to the current row via the rowData argument.
* Remember to parse numbers and dates if you want to make calculations.
*/
let productName = rowData.productName;
let productPrice = parseFloat(rowData.productPrice);
let date = new Date(rowData['delivery date']);
/**
* Return the template. Use the backticks to make use of string interpolation if you want to include context from the current row.
*/
return `
<div class="card">
${productName}
</div>
`;
}
/**
* Use the setCustomTemplate function from the CSL. You can the following five arguments:
* templateFunction: this is the function declared above.
* containerClass: optional, css classes which are applied on the container only once.
* elementClass: optional, css classes which are applied on each element in the loop.
* emptyTemplateFunction: optional, this is the function which will be executed when there are no rows.
* customTemplateProperties: optional, extra properties.
*/
CSL.view.setCustomTemplate(templateFunction, 'row', 'col-sm-6 col-lg-4');
Custom Renderer HTML Structure
The default HTML structure of the custom renderer is a follows:
<div class="containerClass">
<span class="elementClass">
<your-template></your-template>
<span>
</div>
For every record returned a new span
will be rendered. If the method returns three records you will get the following:
<div class="containerClass">
<span class="elementClass">
<your-template></your-template>
<span>
<span class="elementClass">
<your-template></your-template>
<span>
<span class="elementClass">
<your-template></your-template>
<span>
</div>
By specifying the containerClass and/or elementClass you can make the template responsive by using the grid system of Bootstrap. You probably only use this when your method returns more than one row.
Multiple Records at Once
It is also possible to configure multipleRecordsAtOnce
. When this parameter is set to true, your template won't be repeated for the number of records returned by the method. The template will be executed only once and the first two arguments of the template function will be arrays instead of objects.
/**
* Declare a function in a new variable. This function needs to return the HTML as a string.
* Optional - Pass in three arguments: records, formattedRecords and csl.
* rows gives you access to all rows. All values are invariant and in string format.
* formattedRows gives you access to all rows. All values are formatted as configured in the view formatter.
* csl gives you access to the entire CSL library.
*/
let templateFunction = function(rows, formattedRows, csl) {
rows.forEach(row => {
// You have access to the each row ...
});
return `Some kind of HTML template`;
}
/**
* Use the setCustomTemplate function from the CSL. You can the following five arguments:
* templateFunction: this is the function declared above.
* containerClass: optional, css classes which are applied on the container only once.
* elementClass: optional, css classes which are applied on each element in the loop.
* emptyTemplateFunction: optional, this is the function which will be executed when there are no rows.
* customTemplateProperties: optional, extra properties.
*/
CSL.view.setCustomTemplate(templateFunction, null, null, null, { multipleRecordsAtOnce: true });
No Result Message
When there are no results, the default behaviour is that the no results message is displayed from the multi record view. However, it is possible to specify a custom template for this.
const templateFunction = () => `<div>My Custom Record Template</div>`;
// This is the function executed when there are no rows.
const emptyTemplateFunction = () => `<p>Sorry there are no rows.</p>`;
// Pass the emptyTemplateFunction as fourth argument.
CSL.view.setCustomTemplate(templateFunction, '', '', emptyTemplateFunction);
Defining Functions
Sometimes you want to include event listeners such as click handlers. The only way to support this is to create functions on the global window object. The advise here is to create your own namespace to minimize the risk of conflicts.
// Code to make sure your namespace is not overwritten by other components on the page using the same namespace.
window.myNamespace = typeof (window.myNamespace) === 'undefined' ? {} : window.myNamespace;
// Declare a function inside your namespace.
window.myNamespace.processAction = function(htmlElement) {
// Do something ...
// If you pass `this` as argument in the onclick function, you get access to the current HTML element.
}
// Use the function somewhere in the template.
return `
<div class="card" onclick="window.myNamespace.processAction(this)">
</div>
`;
Bootstrap Light
A custom version of Bootstrap 4.4.1 is loaded into the application to ensure some basic functionality. The following is included:
Default Card
The following is one of the most basic cards there is. The card contains of a title, subtitle and text.
const templateFunction = function(rowData, formattedData, csl) {
let title = rowData['title'];
let subTitle = rowData['subTitle'];
let body = rowData['body'];
return `
<div class="card">
<div class="card-body">
<h5 class="card-title">${title}</h5>
<h6 class="card-subtitle mb-2 text-muted">${subTitle}</h6>
<p class="card-text">
${body}
</p>
</div>
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction, 'row', 'col-sm-6 col-lg-4');
Action Card
The following is a more advanced type of card displaying a card with a header, body and two action buttons.
const templateFunction = function(rowData, formattedData, csl) {
return `
<div class="card">
<div class="card-header bg-white">
<h4 class="float-left mb-0">Card title</h4>
<div class="float-right">
<a href="#" class="btn btn-primary btn-sm">Action 1</a>
<a href="#" class="btn btn-secondary btn-sm ml-2">Action 2</a>
</div>
</div>
<div class="card-body">
<p class="card-text">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam deleniti fugit incidunt, iste, itaque minima neque pariatur perferendis sed suscipit velit vitae voluptatem. A consequuntur, deserunt eaque error nulla temporibus!
</p>
</div>
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction, 'row', 'col-sm-6 col-lg-4');
Colour Card
The following cards make use of the border-
and bg-
classes to emphasize a certain card.
const templateFunction = function() {
return `
<div class="row">
<div class="col-md-6 col-lg-2">
<div class="card border-primary mb-3" style="max-width: 18rem;">
<div class="card-header">Header</div>
<div class="card-body text-primary">
<h5 class="card-title">Primary card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-2">
<div class="card border-warning mb-3" style="max-width: 18rem;">
<div class="card-header">Header</div>
<div class="card-body text-warning">
<h5 class="card-title">Warning card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-2">
<div class="card border-info mb-3" style="max-width: 18rem;">
<div class="card-header">Header</div>
<div class="card-body text-info">
<h5 class="card-title">Info card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-2">
<div class="card text-white bg-success mb-3" style="max-width: 18rem;">
<div class="card-header">Header</div>
<div class="card-body">
<h5 class="card-title">Success card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-2">
<div class="card text-white bg-dark mb-3" style="max-width: 18rem;">
<div class="card-header">Header</div>
<div class="card-body">
<h5 class="card-title">Dark card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div>
</div>
</div>
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction);
Card with Progress
The following card combines a header and a progress bar. It also has a fixed with of 200 pixels.
const templateFunction = function() {
return `
<div class="card text-center" style="width: 200px">
<h6 class="card-header">World Wide Closed</h6>
<div class="card-body p-3">
<div class="h2 mb-1">20</div>
<div class="text-muted mb-3">Total Sold</div>
<div class="progress mx-auto mb-3" style="width: 80%">
<div class="progress-bar" role="progressbar" style="width: 72%;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">72%</div>
</div>
<i class="fas fa-caret-up fa-2x" style="color: #5eba00;"></i>
</div>
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction);
Gallery
The gallery displays multiple cards in the form of image cards. By applying the col-sm-6 col-lg-4
as elementClass the gallery is responsive.
On small devices it displays two next to each other, on large screens there are three cards next to each other.
const templateFunction = function(rowData, csl) {
let id = rowData['photoId'];
let name = rowData['name'];
let description = rowData['description'];
let views = rowData['views'];
let likes = rowData['likes'];
let avatarUrl = rowData['avatar url'];
/**
* Navigates to the details page for the landscape.
*/
navigateToLandscape = function(id) {
window.location = `myUrl/gallery/${id}`;
}
/**
* Process hitting the like button.
*/
processLike = function(id) {
csl.rest.executeMethod('action', 'process like');
}
return `
<style>
.avatar {
border-radius: 50%;
background: no-repeat center/cover;
position: relative;
display: inline-block;
width: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;
}
</style>
<div class="card m-1 p-2">
<a href="javascript:navigateToLandscape(${id})" class="mb-3">
<img src="myUrl/gallery/${id}.jpg" alt="${name}" class="card-img-top">
</a>
<div class="d-flex align-items-center px-2">
<div class="avatar mr-3" style="background-image: url(${avatarUrl})"></div>
<div>
<div>${name}</div>
<small class="d-block text-muted">${description}</small>
</div>
<div class="ml-auto text-muted">
<i class="fas fa-eye mr-1"></i> ${views}
<a href="javascript:processLike(${id})" class="icon d-none d-md-inline-block ml-3"><i class="fas fa-thumbs-up mr-1"></i> ${likes}</a>
</div>
</div>
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction, 'row', 'col-sm-6 col-lg-4');
Three Cards with KPI Colours
The following is an example where three cards are displayed. Depending on the value, the cards will have a different background colour.
const templateFunction = function(rowData, formattedData) {
const newCustomers = parseInt(rowData['New Customers']);
const salesRevenue = parseInt(rowData['Sales Revenue']);
const profit = parseInt(rowData['Profit']);
/**
* 5000+ = good
* 500 - 5000 = not so good
* -500 = terrible
*/
function processKpi(value) {
switch (true) {
case (value < 500):
return '#d9534f';
case (value < 5000):
return '#f0ad4e';
case (value >= 5000):
return '#5cb85c';
}
}
return `
<div class="row row-cards">
<div class="col-sm-4 col-lg-4">
<div class="card" style="color:white; background-color:${processKpi(newCustomers)}">
<div class="card-body p-5 text-center">
<div class="h1 m-0">${formattedData['New Customers']}</div>
<div>New Customers | YTD</div>
</div>
</div>
</div>
<div class="col-sm-4 col-lg-4">
<div class="card" style="color:white; background-color:${processKpi(salesRevenue)}">
<div class="card-body p-5 text-center">
<div class="h1 m-0">${formattedData['Sales Revenue']}</div>
<div>Sales Revenue | YTD</div>
</div>
</div>
</div>
<div class="col-sm-4 col-lg-4">
<div class="card" style="color:white; background-color:${processKpi(profit)}">
<div class="card-body p-5 text-center">
<div class="h1 m-0">${formattedData['Profit']}</div>
<div>Profit | YTD</div>
</div>
</div>
</div>
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction);
Six Cards with Click Interaction
The following is an example where six cards are displayed. When clicking on a card, it gets active. Only one card can be active.
const templateFunction = function(rowData, formattedData) {
// Isolate global function in a namespace to minimize the risk of conflicts.
window.myNamespace = typeof (window.myNamespace) === 'undefined' ? {} : window.myNamespace;
// Tracker for current active card, so it can be removed when another card is clicked.
window.myNamespace.activeCard = null;
/**
* Make sure the clicked card is active.
*/
window.myNamespace.toggleActive = function(div) {
// Only one card can be active.
if (window.myNamespace.activeCard !== null && window.myNamespace.activeCard !== div) {
window.myNamespace.activeCard.style.backgroundColor = 'white';
window.myNamespace.activeCard.style.color = '#495057';
}
// rgb(102, 102, 102) means the card is active.
if (div.style.backgroundColor === 'rgb(102, 102, 102)') {
div.style.backgroundColor = 'white';
div.style.color = '#495057';
window.myNamespace.activeCard = null;
} else {
div.style.backgroundColor = 'rgb(102, 102, 102)';
div.style.color = 'white';
window.myNamespace.activeCard = div;
}
}
/**
* Returns a new card element.
*/
function createCard(subTitle, title, percentage) {
/**
* Gets the correct icon and apply correct styling based on the percentage.
*/
function getFaIcon(percentage) {
if (percentage == 0) { return { icon: 'far fa-circle', style: 'color: #a7a7a7;' }; }
if (percentage < 0) { return { icon: 'fas fa-chevron-down', style: 'color: #cd201f !important;' }; }
if (percentage > 0) { return { icon: 'fas fa-chevron-up', style: '#5eba00 !important;' }; }
}
return `
<div class="col-6 col-sm-4 col-lg-2">
<div class="card" style="cursor:pointer;" onclick="window.myNamespace.toggleActive(this)">
<div class="card-body p-3 text-center">
<div class="text-right" style="${getFaIcon(percentage).style}">
${percentage}%
<i class="${getFaIcon(percentage).icon}"></i>
</div>
<div class="h3 m-0">${title}</div>
<div class="mb-4">${subTitle}</div>
</div>
</div>
</div>`;
}
// Here you probably read the `rowData` and loop through the collection.
let cards = createCard('Drafts', '450.00', -9);
cards += createCard('Awaiting Approval', '2,658.00', 16);
cards += createCard('Awaiting Payment', '9,195.57', 24);
cards += createCard('Overdue', '2,990.50', 0);
cards += createCard('Tickets', '16', -26);
cards += createCard('Sales Orders', '36', 13);
return `
<div class="row row-cards">
${cards}
</div>
`;
}
CSL.view.setCustomTemplate(templateFunction);