Build Your Own
The OrchestraCMS components can be extended by developers by adding to the existing components using the provided extension points. Additionally, developers can create their own Lightning components and extend them using elements of the OrchestraCMS Lightning components.
The following example creates a Lightning component to load content as a slideshow. Since the OrchestraCMS Content Loader component already loads content, the features and settings of that component can be co-opted for use in this component.
The component element
<!--
Example of wrapping an OrchestraCMS component to provide extra functionality
-->
<aura:component description="A slider/slideshow component that uses the Dynamic Content Loader to load content by type or taxonomy path" implements="flexipage:availableForAllPageTypes,forceCommunity:availableForAllPageTypes" access="global">
<!-- pass-through attributes for Content Loader -->
<aura:attribute name="siteName" type="String" access="global" />
<aura:attribute name="contentType" type="String" required="true" access="global" />
<aura:attribute name="contentLayout" type="String" required="true" access="global" />
<aura:attribute name="tagPath" type="String" access="global" />
<aura:attribute name="depth" type="Integer" default="5" access="global" />
<aura:attribute name="componentHeading" type="String" default="" access="global" />
<aura:attribute name="columnCount" type="String" default="1 column" access="global" />
<aura:attribute name="order" type="string" default="Original Publish Date" access="global" />
<aura:attribute name="ignoreTargets" type="Boolean" default="false" access="global" />
<aura:attribute name="limitCount" type="Integer" default="5" access="global" />
<aura:attribute name="displayDetail" type="String" default="On SObject Detail Page" access="global" />
<aura:attribute name="customDetail" type="String" access="global" />
<aura:attribute name="displayUnreadStatus" type="Boolean" default="false" access="global" />
<aura:attribute name="displayLikeButton" type="Boolean" default="false" access="global" />
<aura:attribute name="displayChatterFeed" type="Boolean" default="false" access="global" />
<aura:attribute name="instanceName" type="String" access="global" />
<!-- custom attributes for this sample component -->
<aura:attribute name="autoAdvance" type="Boolean" default="true" access="global" description="Enable or disable advancement of slides automatically on a timer" />
<aura:attribute name="advanceDelay" type="Integer" default="5000" access="global" description="Milliseconds between automatic advancement of slides" />
<aura:attribute name="contentLoadCount" type="Integer" default="0" access="private" description="The number of content reported by ContentLoader" />
<aura:attribute name="visibleItem" type="Integer" default="0" access="private" description="The index of the current visible content slide" />
<aura:attribute name="autoAdvanceTimer" type="Object" access="private" description="Javascript setTimeout object" />
<aura:attribute name="contentLoader" type="Aura.Component" access="private" description="The ContentLoader component we're wrapping" />
<!-- handled events -->
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
<aura:handler name="ComponentLoadedEvt" event="cms:ComponentLoadedEvt" action="{!c.componentLoaded}" />
<lightning:buttonIcon iconName="utility:refresh" alternativeText="Refresh" onclick="{!c.refresh}" />
<div class="sliderContainer" id="{!'slider-' + globalId}">
<div class="sliderContent" aura:id="sliderScrollContainer">
{!v.contentLoader}
</div>
<div class="sliderControls">
<aura:if isTrue="{!v.contentLoadCount > 1}">
<button class="sliderButton sliderButtonLeft" onclick="{!c.scrollLeft}" aura:id="sliderButtonLeft">
<lightning:icon iconName="utility:chevronleft" alternativeText="Previous" size="small" />
</button>
<button class="sliderButton sliderButtonRight" onclick="{!c.scrollRight}" aura:id="sliderButtonRight">
<lightning:icon iconName="utility:chevronright" alternativeText="Next" size="small" />
</button>
</aura:if>
</div>
</div>
</aura:component>
The cms:ComponentLoadedEvt fires when the content has loaded into the loader. There are two properties for the cms:ComponentLoadedEvt event.
- contentLoadCount is an integer of how many items were loaded
- hasMore is a boolean indicating more content matching the call criteria is available than has been loaded
The styling element
.THIS {
position: relative;
overflow: hidden;
}
.THIS .contentLoaderItems { /* contentLoaderItems comes from ContentLoader */
display: block;
white-space: nowrap;
transition: all 500ms ease-in-out; /* scroll speed */
padding: 0;
}
.THIS .contentLoaderItems > .ocmsContent_X { /* ocmsContent_X wraps each content item */
display: inline-block;
vertical-align: top;
width: calc(100% + 1px);
padding: 0;
}
.THIS .contentLoaderItems > .ocmsContent_X * {
/* reset text formatting */
white-space: initial;
text-indent: initial;
}
.THIS .sliderButton {
position: absolute;
top: 0;
bottom: 0;
border: 0;
background-color: rgba(0, 0, 0, 0);
transition: all 200ms linear;
outline: none;
}
.THIS .sliderButton svg {
fill: rgba(0, 0, 0, .3);
}
.THIS:hover .sliderButton {
background-color: rgba(0, 0, 0, .05);
}
.THIS:hover .sliderButton svg {
fill: #fff;
}
.THIS .sliderButton:hover {
background-color: rgba(0, 0, 0, .2);
}
.THIS .sliderButtonLeft {
left: 0;
}
.THIS .sliderButtonRight {
right: 0;
}
The design element
<design:component label="SampleContentLoaderSlider"> <design:attribute name="contentType" label="Content Type" datasource="apex://cms.AuraGetSupportedContentTypes" description="Enter the Type label of the OrchestraCMS Content items you wish to populate." /> <design:attribute name="contentLayout" label="Content Template" datasource="apex://cms.AuraGetSupportedContentTemplates" description="Enter the Template label of the OrchestraCMS Content items you wish to display." /> <design:attribute name="tagPath" label="Taxonomy Starting Path" description="Enter the full path of the taxonomy category where this component will begin displaying Content items. eg. /Taxonomy Name/Category/Category" /> <design:attribute name="depth" label="Taxonomy Levels to Include" min="1" max="5" default="5" description="Determines how many hierarchical taxonomy category levels the component will use when populating content. Enter a value from 1 to 5." /> <design:attribute name="autoAdvance" label="Automatically Advance Slides" description="Automatically advance between slides based on a timer." /> <design:attribute name="advanceDelay" label="Slide Advance Delay (ms)" description="Amount of time to pause on a slide before advancing to the next (in milliseconds)" /> <design:attribute name="componentHeading" label="Heading" description="Enter a heading to appear at the top of this dynamically loaded content." /> <design:attribute name="order" label="Sort Order" datasource="Original Publish Date, Alphabetical, Priority Original Publish Date, Published Start Date" /> <!-- ensure values match in helper.setOrderApiName --> <design:attribute name="ignoreTargets" Label="Ignore Targets" description="When selected, targets applied to content are ignored and all content is displayed." /> <design:attribute name="limitCount" label="Item Load Count" default="5" description="The number of items displayed when the page loads. It is recommended that the total number of items does not exceed 50." /> <design:attribute name="displayDetail" label="Display Content Detail" datasource="On SObject Detail Page, On Custom Detail Page, As Modal Dialog Window" description="Select how Content details will display." /> <!-- ensure helper.getDetailType matches values! --> <design:attribute name="customDetail" label="Custom Detail Page URL" /> <design:attribute name="displayUnreadStatus" label="Display Unread Status" description="Display a flag that indicates if the current user has not viewed a content item." /> <design:attribute name="displayLikeButton" label="Display Likes" description="Display a Like button and Like count for the Content item." /> <design:attribute name="displayChatterFeed" label="Display Chatter Comments" description="Display the Content's chatter feed when the Content item is loaded inline." /> <design:attribute name="instanceName" label="Taxonomy Menu ID" description="Enter a unique value to link this Taxonomy Menu to a Dynamic Taxonomy Loader on this page." /> <design:attribute name="siteName" label="OrchestraCMS Site Name" datasource="apex://cms.AuraGetOrchestraCMSSites" description="Enter the name of an OrchestraCMS site to map this component to it. The component will get content from that site. Leave blank for default mapping, which matches names of the Community’s force.com site and the OrchestraCMS site." /> </design:component>
There are three potential datasources.
- apex://cms.AuraGetSupportedContentTypes is a list of OrchestraCMS Content Types that have at least one Content Template that is Lightning enabled
- apex://cms.AuraGetSupportedContentTemplates is a list of OrchestraCMS Content Templates that are Lightning enabled. This means the cms__Render_Context__c field of the cms__Content_Layout__c record has LIGHTNING as a value
- apex://AuraGetOrchestraCMSSites is a list of OrchestraCMS sites
The controller element
({
doInit: function(component, event, helper) {
$A.createComponent(
'cms:ContentLoader',
{
//These are getting the values configured in the component but could also be directly coded values instead
contentType: component.get('v.contentType'),
contentLayout: component.get('v.contentLayout'),
tagPath: component.get('v.tagPath'),
depth: component.get('v.depth'),
order: component.get('v.order'),
ignoreTargets: component.get('v.ignoreTargets'),
limitCount: component.get('v.limitCount'),
showLoadMore: false,
displayDetail: component.get('v.displayDetail'),
customDetail: component.get('v.customDetail'),
displayUnreadStatus: component.get('v.displayUnreadStatus'),
displayLikeButton: component.get('v.displayLikeButton'),
displayChatterFeed: component.get('v.displayChatterFeed'),
instanceName: component.get('v.instanceName'),
siteName: component.get('v.siteName'),
overrideLoadingComponent: 'c:SampleLoadingSpinner',
// developerOverrideString: '{"loadingComponent":"c:SampleLoadingSpinner"}'
},
function (newCmp, status, errorMessage) {
if (status === "SUCCESS") {
component.set('v.contentLoader', newCmp);
} else if (status === "INCOMPLETE") {
console.log("No response from server or client is offline.");
// Show offline error
} else if (status === "ERROR") {
console.log("Error: " + errorMessage);
component.set('v.overrideComponentName', null);
}
}
);
},
componentLoaded: function(component, event, helper) {
// Only act on events sent from our content loader. This event is also fired when loading the detail view of inline content
if (event.getSource().getGlobalId() === component.get('v.contentLoader').getGlobalId()) {
helper.setupComponent(component, event, helper);
}
},
refresh: function(component, event, helper) {
var contentLoader = component.get('v.contentLoader');
contentLoader.reInit();
},
scrollLeft: function(component, event, helper) {
helper.advanceSlides(component, helper, -1);
},
scrollRight: function(component, event, helper) {
helper.advanceSlides(component, helper, 1);
}
})
The createComponent is instantiating one of the OrchestraCMS components with the options set. The options can have the values preset or the developer can specify them as configuration fields on the component so that they can be set in Community Builder. OrchestraCMS provides five components that can be instantiated.
- Content loads content by name, if multiple pieces of content have they same name they will all be loaded
- ContentLoader is typically used to render multiple content items
- ContentViewer is used to render content by the content originId read from the URL parameter id
- SearchResults is used to render a list of content search results
- TaxonomyMenu is used to render taxonomy categories as a menu for use with a content loader
The helper element
({
setupComponent: function(component, event, helper) {
function debounce(func, wait, immediate) {
// https://davidwalsh.name/javascript-debounce-function
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
// if the browser window changes size, reset the slider's position so that the current slide is visible
var resizeHandler = debounce(function() {
helper.advanceSlides(component, helper, 0);
}, 100);
window.addEventListener('resize', resizeHandler);
component.set('v.contentLoadCount', event.getParam('contentLoadCount'));
helper.startAutoAdvanceTimer(component, helper);
},
startAutoAdvanceTimer: function(component, helper) {
var autoAdvanceTime = parseInt(component.get('v.advanceDelay'));
if (component.get('v.autoAdvance') && autoAdvanceTime > 0) {
var timer = component.get('v.autoAdvanceTimer');
clearTimeout(timer);
timer = setTimeout($A.getCallback(function () {
if (component.isValid()) {
helper.advanceSlides(component, helper, 1);
}
}),
autoAdvanceTime);
component.set('v.autoAdvanceTimer', timer);
}
},
advanceSlides: function(component, helper, advanceBy) {
try {
var container = component.find('sliderScrollContainer');
if (container) {
var scrollContainer = container.getElement(),
itemCount = component.get('v.contentLoadCount'),
visibleItem = component.get('v.visibleItem'),
nextItem = visibleItem + advanceBy;
// wrap around and start at the beginning or end if necessary
if (nextItem > itemCount - 1) {
nextItem = 0;
} else if (nextItem < 0) {
nextItem = itemCount - 1;
}
// compute the width of the slides and use that as an offset to move the current slide in view
// scroll speed is controlled via css transition
var position = parseFloat(scrollContainer.clientWidth) * nextItem;
scrollContainer.style.textIndent = (position / -1) + 'px';
component.set('v.visibleItem', nextItem);
helper.startAutoAdvanceTimer(component, helper); // restart the timer
}
} catch (e) {}
}
})