Jump to content


Customizing our listing templates

How the Release Notes section is structured

Before we start editing the templates, it would be helpful to understand how the finished section is going work. By default in Pages database, you see a record listing (which uses the Listing templates), you click through to a record which loads a new page to show it (which uses the Display templates). In our Release Notes database however, there's no page reload - the record is loaded dynamically via AJAX.

This is achieved by mixing both the Listing and Display templates. The wrapper of the page and the version list on the left is built using the Listing templates. However, when a record is clicked and loaded dynamically, that portion of the page is using the Display template:


Now that the structure is clear, it's finally time to start editing templates!


Each template discussed below and in the next step includes a link to a diff report so that you can easily compare the changes we've made to the default template.


Listing header - categoryHeader template (See diff)

The first template we'll work on is the category header. This shows the header of the category (which in our case is actually the header of the whole section, since the user doesn't leave this page while using the database). The categoryHeader template handles the title, the follow button, the Add Record button and more.

Here's the final code for this template bit, which I'll explain below. 


{{if !\IPS\Request::i()->advancedSearchForm}}
	<div class="ipsType_center ipsSpacer_bottom ipsSpacer_top">
		<h1 class="ipsType_veryLarge ipsType_reset">{$category->_title}</h1>
		<div class="ipsType_richText ipsType_large ipsType_light ipsSpacer_bottom">
		<div class='ipsResponsive_noFloat ipsResponsive_hidePhone'>
			{template="follow" app="core" group="global" params="'cms','categories' . $category->database_id, $category->_id, \IPS\cms\Records::containerFollowerCount( $category )"}

{{if $category->hasChildren() AND ! isset( \IPS\Request::i()->advancedSearchForm )}}
	<div class="ipsBox ipsSpacer_bottom">
		<h2 class='ipsType_sectionTitle ipsType_reset'>{lang="content_subcategories_title"}</h2>
		<ol class="ipsDataList">
			{{foreach $category->children() as $cat}}
				{template="categoryRow" group="category_index" location="database" app="cms" params="$cat"}

{{if $category->can('add')}}
	{{if ! \IPS\Request::i()->isAjax() AND ! isset( \IPS\Request::i()->advancedSearchForm ) AND $category->show_records}}
		<ul class="ipsToolList ipsToolList_horizontal ipsClearfix ipsSpacer_both ipsResponsive_hidePhone">
			<li class='ipsToolList_primaryAction'>
				<a class="ipsButton ipsButton_medium ipsButton_important ipsButton_fullWidth" href="{$category->url()->setQueryString( array( 'do' => 'form', 'd' => \IPS\cms\Databases\Dispatcher::i()->databaseId ) )}">{lang="cms_add_new_record_button" sprintf="\IPS\cms\Databases::load( $category->database_id )->recordWord( 1 )"}</a>

{{if count( $activeFilters )  AND ! isset( \IPS\Request::i()->advancedSearchForm )}}
	{template="filterMessage" app="cms" location="database" group="release_notes" params="$activeFilters, $category"}


This template is pretty similar to the default. The key things that have changed:

  • Positioning/styling of the title, description and follow button
  • I have removed the list item that contained the 'mark as read' link, since we don't use this functionality
  • I have wrapped the tool list bar in {{if $category->can('add')}} since only those with permission to add records need to see these elements.


Listing table - categoryTable template (See diff)

The categoryTable template is the 'meat' of the listing view; it generates the table into which record rows are displayed. This template is modified quite heavily from the default, in part due to a lot of unused code being removed.

Note: templates often contain a lot of code that is used when a certain option is enabled for the database. This code is wrapped in a logic check so it's only displayed if that option is enabled. It is my preference to remove these unused portions of code when I'm certain I don't need the feature they display; it makes the template more succinct and easier to read.

Here's the final code for this template bit:


<div class='ipsAreaBackground ipsPad_half' data-baseurl='{$table->baseUrl}' data-resort='{$table->resortKey}' data-controller='core.global.core.table{{if $table->canModerate()}},core.front.core.moderation{{endif}}'>
	<div class='ipsAreaBackground_reset ipsColumns ipsColumns_collapsePhone' data-controller='pages.front.releaseNotes.main'>
		<div class='ipsColumn ipsColumn_wide ipsAreaBackground cReleaseColumn' data-role='releases'>
			{{if ! count($rows)}}
				<div class="ipsPad">
					{lang="cms_no_records_to_show" sprintf="\IPS\cms\Databases::load( \IPS\cms\Databases\Dispatcher::i()->databaseId )->recordWord()"}
				<ol class='ipsDataList ipsDataList_zebra ipsClear cCmsListing {{foreach $table->classes as $class}}{$class} {{endforeach}}' id='elTable_{$table->uniqueId}' data-role="tableRows">
					{template="$table->rowsTemplate[1]" params="$table, $headers, $rows" object="$table->rowsTemplate[0]"}
			{{if $table->pages > 1}}
				<div data-role="tablePagination">
					{template="pagination" group="global" app="core" location="global" params="$table->baseUrl, $table->pages, $table->page, $table->limit"}
		<div class='ipsColumn ipsColumn_fluid'>
			<div data-role='releaseInfo' class='ipsPad_double'></div>


Here's the key changes:

  • There's some simple style changes to get the output to look how we want
  • Note that on the wrapper element, we initialize a Javascript controller, pages.front.releaseNotes.main. This controller will be created in the Javascript file we set up earlier.
  • A lot of code we don't need for our use is removed; the moderation tools and sorting/filter options.
  • An empty div is added in the fluid column, with a data-role attribute. This is the div that will hold a record when it is loaded into the page.


Listing row - recordRow template (See diff)

This template generates each row of the listing table. It's the first template where we're doing something special, so I'll cover those parts below. Here's the final code:


{{$rowIds = array();}}
{{foreach $rows as $row}}
	{{$idField = $row::$databaseColumnId;}}
	{{$rowIds[] = $row->$idField;}}
{{$iposted = ( $table AND method_exists( $table, 'container' ) AND $table->container() !== NULL ) ? $table->container()->contentPostedIn( null, $rowIds ) : array();}}

{{foreach $rows as $row}}
	{{$idField = $row::$databaseColumnId;}}
	<li class="cCmsRecord_row {{if $row->hidden()}}ipsModerated{{endif}}" data-rowID='{$row->$idField}'>
		<a href='{$row->url()}' class='cRelease' data-releaseID='{$row->$idField}' {{if $row->fieldValues()['field_163']}}data-currentRelease{{endif}}>
			{$row->customFieldDisplayByKey('security-release', 'listing')|raw}
			<h3 class='ipsType_sectionHead ipsType_break'>
				{{if $row->_title}}{$row->_title}{{else}}<em class="ipsType_light">{lang="content_deleted"}</em>{{endif}}
				{$row->customFieldDisplayByKey('current-release', 'listing')|raw}
              	{$row->customFieldDisplayByKey('beta-release', 'listing')|raw}
			{{if $row->isFutureDate() || $row->mapped('pinned') || $row->mapped('featured') || $row->hidden() === -1 || $row->hidden() === 1}}
					{{if $row->isFutureDate()}}
						<span class="ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_warning" data-ipsTooltip title='{$row->futureDateBlurb()}'><i class='fa fa-clock-o'></i></span>
					{{elseif $row->hidden() === -1}}
						<span class="ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_warning" data-ipsTooltip title='{$row->hiddenBlurb()}'><i class='fa fa-eye-slash'></i></span>
					{{elseif $row->hidden() === 1}}
						<span class="ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_warning" data-ipsTooltip title='{lang="pending_approval"}'><i class='fa fa-warning'></i></span>
					{{if $row->mapped('pinned')}}
						<span class="ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_positive" data-ipsTooltip title='{lang="pinned"}'><i class='fa fa-thumb-tack'></i></span>
					{{if $row->mapped('featured')}}
						<span class="ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_positive" data-ipsTooltip title='{lang="featured"}'><i class='fa fa-star'></i></span>
			{{if count( $row->customFieldsForDisplay('listing') )}}
				<div class='ipsDataItem_meta'>
				{{foreach $row->customFieldsForDisplay('listing') as $fieldId => $fieldValue}}
					{{if $fieldValue && $fieldId != 'current-release' && $fieldId != 'beta-release' && $fieldId != 'security-release'}}


As with the previous template, a lot of unused code has been removed.

The first few lines are setup that also exist in the default. Following those, we go into the main loop which iterates over each of our rows and generates the HTML for each.

The first part I want to highlight is the <a> element. Notice we've applied a custom classname which we'll use for styling. We've also added a data-releaseID attribute, the value of which is the ID of the record. This is important because we'll use it to load the correct record later when the user clicks this record in the listing.


Accessing raw field values

On the same line, you'll see this:

{{if $row->fieldValues()['field_163']}}data-currentRelease{{endif}}

What's happening here is we are calling $row->fieldValues() (which returns an array of all the field values for the row), and using it to determine if field_163 is true. If it is, we mark this as our current release with a data attribute. You would be correct to assume then that field 163 is the current release field we set up. Unfortunately we have to refer to it by ID rather than key when using the fieldValues() method, which does complicate this check slightly.

Note: In your own database, the current release field will have a different ID. Replace field_163 with whatever the field ID is in your setup.


Accessing formatted field output

A few times within this template, you'll see a call like this:

{$row->customFieldDisplayByKey('security-release', 'listing')|raw}

This is returning the output of one of our custom fields (the security_release toggle in this case). It returns the generated HTML from our formatting options that we set up for the field. This is what makes custom formatters so useful - we can keep the output for the field neatly managed on the field edit screen, and simply insert it into our templates with the simple tag above. Notice we pass the listing value for the second parameter. This is indicating we want the listing format (if you recall, you can create formatting for both the listing and display templates). 


  Report Guide