Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

Dealing with arbitrary template data

In this lesson: ViewableData, ArrayList, ArrayData, SS_List
Silverstripe CMS version: 4.x (stable)

In this tutorial, we're going to talk about adding non-database content to our templates.

UncleCheese

What we'll cover

  • What are we working towards?
  • Updating the template: Working backwards
  • Updating the controller to use generic data

What are we working towards?

Up until now, the data on our templates has been pretty one-sided. It's sourced from the database, and we render the fields from one or many returned records on the template. However, the template and the database are not so tightly coupled. There's no rule saying that all template data has to come from the database

Ultimately what we're teaching in this lesson is the concept of composable UI elements. As you may know, composable components are a rapidly accelerating trend in application development as developers and designers seek to maintain a high level of agility and reusability.

Being composable, these components are essentially "dumb" and only really know how to do one thing, which is render some UI based on the configuration that has been passed to them, which is what we'll call a composition.

In the context of our project, we'll be lighting up the search filter toggle buttons in the sidebar of our property search page. The purpose of these buttons is to show the user what search filters have been applied, and to offer an option to remove them and refresh the search page.

Updating the template: Working backwards

A lot of developers, including myself, find it easier to work backwards with problems like this, which means starting from the template and adding the backend afterward. Let's look at these filter buttons and try to abtract them into something we can use.

As we can see, they're all statically contained in a ul tag at the moment.

<ul class="chzn-choices">
   <li class="search-choice"><span>New York</span><a href="#" class="search-choice-close"></a></li>
   <li class="search-choice"><span>Residential</span><a href="#" class="search-choice-close"></a></li>
   <li class="search-choice"><span>3 bedrooms</span><a href="#" class="search-choice-close"></a></li>
   <li class="search-choice"><span>2 bathrooms</span><a href="#" class="search-choice-close"></a></li>
   <li class="search-choice"><span>Min. $150</span><a href="#" class="search-choice-close"></a></li>
   <li class="search-choice"><span>Min. $400</span><a href="#" class="search-choice-close"></a></li>
</ul>

The wrong way to do it

One approach that may come to mind is using a long series of display logic to output all of the possible options, like so:

<ul class="chzn-choices">
<% if $LocationFilter %>
   <li class="search-choice"><span>$LocationFilter</span><a href="#" class="search-choice-close"></a></li>
<% end_if %>

<% if $BedroomFilter %>
   <li class="search-choice"><span>$BedroomFilter bedrooms</span><a href="#" class="search-choice-close"></a></li>
<% end_if %>

<!-- etc... -->
</ul>

This might look reasonable at first, it's going to lead to nothing but problems. There are a number of things wrong with this approach.

  • It pollutes your template with syntax, and a lot of repeated markup
  • It pollutes your controller with a lot of repetative property assignments and/or methods
  • It creates more parity between your controller and your template. If you ever want to add or remove a new search option, you have to remember to update the template.
  • We have to repurpose the value of the filter as its label, e.g. $BedroomFilter bedrooms, and at some point that's just not going to work. Search filters are often not human-readable, such as IDs.

A better approach

If the sight of li tags nested in a ul is becoming almost synonymous with the <% loop %> control to you, that's a good sign. We're definitely going to need a loop here. This will keep the UI template much cleaner, and it will give us more control over the output, as we'll have a chance to compose each member of the loop. Let's add that now, and make up the rest as we go.

<ul class="chzn-choices">
   <% loop $ActiveFilters %>
        <li class="search-choice"><span>New York</span><a href="#" class="search-choice-close"></a></li>
   <% end_loop %>
</ul>

Make sense so far? Again, we're working backwards, so the $ActiveFilters piece is merely semantic right now.

Let's now just go through brainstorm some property names for all the dynamic content.

<% if $ActiveFilters %>
<div class="chzn-container-multi">
  <ul class="chzn-choices">
     <% loop $ActiveFilters %>
        <li class="search-choice"><span>$Label</span><a href="$RemoveLink" class="search-choice-close"></a></li>
     <% end_loop %>
  </ul>
</div>
<% end_if %>

We've added the properties $Label and $RemoveLink, which we can assume are the only two distinguishing traits of each filter button.

Updating the controller

Now that our template syntax is in place, we need to configure the controller to feed this data to the template. We could write a new method called ActiveFilters() (or getActiveFilters()) that inspects the request and returns something, but given that there's only one endpoint for our search page, I think it makes more sense at this point in the project to create the filter UI elements as they're being applied to the list.

Creating an arbitrary list

In order to invoke the <% loop %> block we of course will need some kind of iteratable list. So far we've been using SilverStripe\ORM\DataList, which represents a list of records associated with a database query. Since our filter UI elements are not coming from the database, we'll need something more primitive. In this case, SilverStripe\ORM\ArrayList is an ideal choice.

At the top of our index() action, let's instantiate that list.

app/src/PropertySearchPageController.php

//...
use SilverStripe\ORM\ArrayList;

class PropertySearchPageController extends PageController
{
    public function index(HTTPRequest $request)
    {
        $properties = Property::get();
        $activeFilters = ArrayList::create();

        //...
    }
    //...

Now, we just need to fill our list with data.

Remember ViewableData?

To populate the list, we'll revisit our old friend SilverStripe\View\ViewableData from the previous tutorial. Just as a recap, ViewableData is a primitive object that is ready to be rendered on a template. One type of ViewableData is DataObject, which we've been using all along to render content from the database.

You will rarely need to use the ViewableData class itself, but as its immediate descendant SilverStripe\View\ArrayData is very flexible and couldn't be simpler to implement. It's basically just a glorified keyed array. All you have to do is instantiate it with an array of key/value pairs that will translate to $Variable template variables, and render their associated values.

Let's add the details for the Keywords filter.

app/src/PropertySearchPageController.php

//...
use SilverStripe\View\ArrayData;
use SilverStripe\Control\HTTP;

class PropertySearchPageController extends PageController
{

    public function index(HTTPRequest $request)
    {

        //...

        if ($search = $request->getVar('Keywords')) {
            $activeFilters->push(ArrayData::create([
                'Label' => "Keywords: '$search'",
                'RemoveLink' => HTTP::setGetVar('Keywords', null, null, '&'),
            ]));

            $properties = $properties->filter([
                'Title:PartialMatch' => $search
            ]);
        }

        //..

Using the push() method on ArrayList, we add ArrayData objects to it. Each one has Label and RemoveLink properties, as required by the template. The RemoveLink property implements an obscure utility method from the SilverStripe\Control\HTTP helper class. All it does is take the current URI and set a given query parameter to a given value. In this case, we're setting it to null to effectively remove the filter.

The next filter is for the availability date range. It actually doesn't offer a whole lot of utility to the user to display this as a toggleable filter, especially since it's actually a composite filter of ArrivalDate and Nights, so let's skip this one.

The next several, which are all part of our tidy loop, are pretty straightforward. We'll add another member to the $filterKeys list, which will be a sprintf() compatible template to generate the label for each filter.

app/src/PropertySearchPageController.php

    public function index(HTTPRequest $request)
    {

        //...

    $filters = [
        ['Bedrooms', 'Bedrooms', 'GreaterThanOrEqual', '%s bedrooms'],
        ['Bathrooms', 'Bathrooms', 'GreaterThanOrEqual', '%s bathrooms'],
        ['MinPrice', 'PricePerNight', 'GreaterThanOrEqual', 'Min. $%s'],
        ['MaxPrice', 'PricePerNight', 'LessThanOrEqual', 'Max. $%s'],
    ];

    foreach($filters as $filterKeys) {
        list($getVar, $field, $filter, $labelTemplate) = $filterKeys;
        if ($value = $request->getVar($getVar)) {
            $activeFilters->push(ArrayData::create([
                'Label' => sprintf($labelTemplate, $value),
                'RemoveLink' => HTTP::setGetVar($getVar, null, null, '&'),
            ]));

            $properties = $properties->filter([
                "{$field}:{$filter}" => $value
            ]);
        }
    }

        //...
    }

Passing the filters to the template

Just like our custom variable Results, we'll pass the ActiveFilters list to the template through an array.

app/src/PropertySearchPageController.php

    public function index(HTTPRequest $request)
    {

        //...

        $paginatedProperties = PaginatedList::create(
            $properties,
            $request
        )->setPageLength(15)
         ->setPaginationGetVar('s');

        $data = array (
            'Results' => $paginatedProperties,
            'ActiveFilters' => $activeFilters           
        );

        //...
    }

Reload the page, and you should have working filter buttons now!

Keep learning!

Creating Filtered Views

In this lesson we'll talk about filtering a list of items on a template through the navigation.

Beyond the ORM: Building custom SQL

In this tutorial, we'll build a date archive using custom SQL queries, and introduce the SQLQuery class.

Advanced Environment Configuration

In this lesson, we'll talk about going beyond simple _ss_environment.php configurations and discuss how to deploy targeted functionality toward dev, test, and live environments.