Displaying search results

After the form has been submitted, Ctesius and Hestia will go off and fetch the results - normally ten per page, though this is configurable. A JSON response is returned and Liquid loops through and displays the results.

The first file we will want to get up and running is the properties/index.liquid file, which is located in the properties folder. The index displays every property belonging to an agency or portal. All searches are channel based so an index could show all sales or all lettings.

So the code could look something like this:

{% if properties == empty %}
  <h4>Sorry, we couldn't find any properties for this search.</h4>
 {% else %}
  {% include 'search/results' %}
{% endif %}

Next we need to add the properties loop to search/results.

The properties loop

Since Liquid for loops can include a partial for each property it makes sense to include a tried, tested and mananageble partial of code for each property. We refer to this partial as _property_small.liquid.

Here’s what our for loop looks like:

{% for property in properties %}
 {% include "properties/property_small" %}
{% endfor %}

As you can see we loop through the properties array and include the _property_small partial for each one. Let’s now take a look at what a sample _property_small.liquid could look like:

<div id="property_{{property.property_id}}" class="property-small">
 {{ property | photo_overlay }}
 <a href="{{property | url_for_property}}">
  <img src="{{ property.photos.first | url_for_property_asset: "176x133" }}">
 </a>
 <a href="{{property | url_for_property}}">
  {{property.display_address | truncate : 60}}
 </a>
 <p>{{property.price}}</p>
 <p>{{property.short_description | truncate : 185}}</p>
 <p><a href="{{property | url_for_property}}">Full details</a></p>
</div>
  • property_{{property.property_id}} - outputs the unique ID of the property

  • {{ property | photo_overlay }} - outputs the property status in the form of an image banner. You can customise this in your agency or portal admin with the following link: /configure/website/appearance/custom_images.

  • {{ property.photos.first | url_for_property_asset: "176x133" }} - gets the first photo from the property.photos array and resizes to the specified dimensions.

  • {{property | url_for_property}} - outputs the relative address to the property

  • {{property.display_address | truncate : 60}} - this tag outputs the display address of the property.

  • truncate : 60 - truncates the output to 60 characters. Another useful truncate function is truncatewords: 20.

Locations and postcodes

All we need to do to deal with locations like counties and towns as well as postcodes is add three subfolders in the root directory: locations, counties and postcodes. Within these folders we simply need a show.liquid file and inside this file we add include 'search/results'. This will use our search results partial for the location searches.

Headings and pagination

Headings need to reflect the search the user has carried out or it should output a very general statement if it’s an index search.

<h1>
 {{search.dictionary.general_collective | capitalize}}
 {{search.dictionary.preposition}}
 {% if location %}in {{location.name}}{% endif %}
</h1>

This could output something like:

Houses for sale in Walton on Thames

The general_collective is the property type, the preposition is the channel (sales or lettings) and finally we do a location test and output the in and {{location.name}} if it comes back as true. Let’s look at another example:

{{pagination.from_record}} to {{pagination.to_record}} of {{pagination.total_count}}
Properties found
{% if location %}
 in {{location.name}}
{% endif %}
{% if pagination.has_prev_page %}
    | <a href="{{pagination.previous_page_link}}" class="pagination_prev">
    Previous {{pagination.page_size}}
   </a>
{% endif %}
{% if pagination.has_next_page %}
    | <a href="{{pagination.next_page_link}}" class="pagination_next">
    Next {{pagination.page_size}}
  </a>
{% endif %}

This might output something like:

1 to 12 of 31 Properties found in Walton on Thames | Next 12

More on pagination

Ctesius ships with a for pagination function. If you’ve ever used Google before, and let’s face it, who hasn’t, then you’ll be familiar with the pages before and after pagination they utilise at the foot of the search results.

This function takes a couple of arguements: previous and after.

These arguements allow you to define how many trailing and remaining pages to show. Here’s the code:

{% unless pagination.page_count == 1 %}
 <ul>
  {% if pagination.has_prev_page %}
   {% if pagination.page_count > 2 %}
    <li><a href='{{pagination.first_page_link}}'>&lt; First</a></li>
   {% endif %}
  {% endif %}
  {% for_pagination pagination previous: 4 after : 4 %}
   <li>
    <a href="{% if pagination_item.page_number == pagination.current_page }}#
    {% if pagination_item.page_number == pagination.current_page }}#
    {% else %}{{pagination_item.link}}{% endif %}"
    {% if pagination_item.page_number == pagination.current_page %} class='s'{% endif %}>
    {{pagination_item.page_number}}</a>
   </li>
  {% endfor_pagination %}
  {% if pagination.has_next_page and pagination.current_page != pagination.page_count %}
   {% if pagination.page_count > 2 %}
    <li>
     <a href='{{pagination.last_page_link}}'>Last &gt;</a>
    </li>
   {% endif %}
  {% endif %}
 </ul>
{{ endunless }}

Togglable areas

The core application javascript include comes with a JavaScript toggle system that can show and hide different tabs on a click. This is useful when working with different views and, as we will see later, it is also very useful on the property show page.

There are at least three well defined view types for the results page. They are list, grid and map. Here’s what the code might look like:

<div id='togglable_list' class='togglable_area'>
 {% include 'properties/properties_list' %}
</div>
<div id="properties_grid_toggle_view" class="hidden togglable_area">
 {% include 'properties/properties_grid' %}
</div>
<div id='togglable_map' class='togglable_area hidden'>
 {% include 'properties/pagination_for_map' %}
 <div id='draggable_map_view'></div>
</div>

Then to link to the views, we simply use the hash reference in the anchor like so:

<li><a href="#home" class="active">LIST</a></li>
<li><a href="#grid">GRID</a></li>
<li><a href="#map">MAP</a></li>

Note that #home correlates to the default list view.

Working with map views

To get the map up and running, all you need to do is add the draggable_map_view div to your tab and supply it with a height and a width in your CSS:

<div id='draggable_map_view'></div>

Note that if the draggable map doesn’t sit in the map container, be sure to call #map is the URL for it to display. If you do not want the map to be draggable, and for it to fetch new properties on drag, you can use a static map instead that will render the amount of results on each page:

<script>
 Ctesius.addConfig('small_map_element', 'map')
</script>

The ‘map in quotes there refers to the div to assign the map to.

Given that we’re allowing the user to zoom and drag, it would be useful to have a dialogue of the current properties that are on display:

<span id='map_info'></span>
{% include 'properties/pagination_links' %}

Here we have an empty div ready to be populated with our dynamic map information and we have a partial to include the list, grid and map links that are common to all of the views. All we need to add to get the map information is two Ctesius events:

<script>
 Ctesius.registerEvent('before_draggable_map_updated',function(){
  $('#map_info').html('Updating map...')
 });

 Ctesius.registerEvent('draggable_map_updated',function(res){
  $('#map_info').html('Showing ' + res.properties.length + ' of ' + res.pagination.total_count + ' properties. Zoom in or drag the map to see more.' )
 });
</script>

For more on events, visit the events section.

Working with grid views

The easiest way to get up and running with a grid view is to have a properties_grid partial that sits in the togglable grid container. Within this partial there is a second properties loop:

 <div id="properties_grid_toggle_view" class="hidden togglable_area">
  {% for property in properties %}
   {% include 'properties/properties_grid' %}
  {% endfor %}
 </div>

Another alternative is to use JavaScript to show/hide/tweak the list and grid layouts on the click of a link.

Sorting property results

You can either use a select box or set of anchors with the fields below to order results. Firstly the HTML with a helper function that gets the current search fragment for us and applies the sort URL:

<select class="result_sort">
 <option value="most-expensive-first" 
  {% if search.order == 'most-expensive-first' %}selected="selected"{% endif %} 
  data-url="{{ search | search_url_with_sort : 'most-expensive-first' }}">
   Price high &gt; low
 </option>
 <option value="least-expensive-first" 
  {% if search.order == 'least-expensive-first' %}selected="selected"{% endif %} 
  data-url="{{ search | search_url_with_sort : 'least-expensive-first' }}">
   Price low &gt; high
 </option>
 <option value="most-recent-first" 
  {% if search.order == 'most-recent-first' %}selected="selected"{% endif %} 
  data-url="{{ search | search_url_with_sort : 'most-recent-first' }}">
   Most recent first
 </option>
 <option value="most-recently-updated-first" 
  {% if search.order == 'most-recently-updated-first' %}selected="selected"{% endif %} 
  data-url="{{ search | search_url_with_sort : 'most-recently-updated-first' }}">
   Most recently updated first
 </option>
</select>

Next, a small jQuery function is employed to kick the event:

$('body').on('change', '.result_sort', function(event) {
 var url = $(':selected', $(event.target)).data('url');
 if(url != undefined) {
  window.location.href = url;
 }
});

Saving to a shortlist

In your js_templates folder, add the partial _saved_properties.liquid. In it we need a couple of constructs, the first is:

{% raw %}
 <script id="saved_properties_template" type="text/liquid">
  <div class="content box">
   <h3>My property shortlist</h3>
   <div class="shortlist_inner">
    <div id="property_shortlist" class="content-block">
     <div id='favourite_property_list' class="content"></div>
    </div>
   </div>
  </div>
 </script>
{% endraw %}

Essentially this is our widget container. Here’s the next construct:

{% raw %}
 <script id="saved_property_template" type="text/liquid">
  <div id="mini_property_{{property.property_id}}" class="mini_property mini_property_{{property.property_id}}">
   <div style="position:relative;" class="clearfix">
    <img src="/liquid_assets/images/delete.png" style="display: none;" id="remove_icon_{{property.property_id}}"
    class="remove_icon" onclick="javascript:Ctesius.Actions.removeSavedProperty({{property.property_id}})">
    <a href="{{property.property_url}}"><img src="{{ property.small_photo }}" class="shortlist_img"></a>
    <div class="featured_property_data">
     <a href="{{property.property_url}}">{{ property.bedrooms }} bedrooms</a><br />
     <a href="{{property.property_url}}">{{ property.price }}</a><br />
     <a href="{{property.property_url}}">{{ property.road_name | truncate : 20 }}</a>
    </div>
   </div>
  </div>
 </script>
{% endraw %}

This construct is the actual saved property record within the widget.

Next we need to allow our widget to show up somewhere in your theme - typically a sidebar. Add the following code wherever you would like it to show:

<div id="saved_properties_view" class="hidden"></div>.

This will render the results of the script code seen above within the saved_properties_view div tags.

Next we need to output a save button or link on each property. To do that we add the following code to our _property_small.liquid that we worked with earlier in the documentation (and the grid view if you have one):

<a class="shortlist_link_{{ property.property_id }}"
onclick="Ctesius.Actions.addSavedProperty({{property.property_id}}); return false;">
 Add to Shortlist
</a>

It is now close to working but there’s some events we need to add to js_event_registers to stitch it all together. The first is in an event to do some stuff when the view is rendered (i.e. when the user clicks the add button):

Ctesius.registerEvent('saved_property_view_rendered', function(saved_property){
 $('#saved_properties_view').removeClass("hidden");
 $(".shortlist_link_"+saved_property.id).html("Remove from Shortlist");
 $(".shortlist_link_"+saved_property.id).attr("onclick", 'Ctesius.Actions.removeSavedProperty('+saved_property.id+'); return false;');
 $('#mini_property_'+saved_property.id).mouseenter(function(){$('#remove_icon_'+saved_property.id).show()});
 $('#mini_property_'+saved_property.id).mouseleave(function(){$('#remove_icon_'+saved_property.id).hide()});
});

###Associated Events There are a bunch of event callbacks that are fired when events take place in Ctesius system. This enables you to write custom functions to provide feedback for these events. Below is a list of shortlist events:

  • saved_property_view_rendered - Fired when a saved property has been added.
  • saved_property_removed - Fired when a saved property has been removed.
  • saved_search_added - Fired when a search is saved to the users favourites.
  • saved_search_removed - Fired when a saved search is removed.

Saving searches

Here’s the first bit of code you will need to add where you would like the save search link/button:

{% if location %}
 <li><a onclick="Ctesius.Actions.saveCurrentSearch();" id="save_search">SAVE SEARCH</a></li>
{% endif %}

Then there’s two events we need to provide the user with some feedback:

Ctesius.registerEvent('saved_search_added', function(search, collection){
 if (search.equalTo(Ctesius.getSearch())){
  $('#save_search').html('REMOVE SEARCH');
  $("#save_search").attr("onClick", 'Ctesius.Actions.removeSavedSearch("'+search.search_id()+'");')
 }

 $('#saved_searches_link').effect('highlight',{color: '{{theme_preferences.accent_colour}}'}, 3000);
});

Ctesius.registerEvent('saved_search_removed', function(search, collection){
 if (search.equalTo(Ctesius.getSearch())){
  $("#save_search").html("SAVE SEARCH")
  $("#save_search").attr("onclick", 'Ctesius.Actions.saveCurrentSearch();')
 }
});


Associated Events

To override any default callbacks simply override the following Ctesius events:

  • saved_search_added (fires when a search is added and when the page loads)
  • after_saved_search (fires only after a search has been saved)
  • saved_search_removed (fires when a search is removed)

Infinite scrolling

The first step to get up and running is to add a Ctesius addConfig. This can go anywhere (as long as it’s included in some way on the properties index page). It’s usually a good idea to keep these config settings together in a single partial and include this in your main layout file (application.liquid).

Ctesius.addConfig('enable_infinite_scroll', true)

The second step is to wrap your properties results loop in an infinite_pages ID. In a very basic form:

<div id="infinite_pages">
 <div class="infinite_page">
  <ul>
   {% for property in properties %}
    {% include "properties/property_medium" %}
   {% endfor %}
  </ul>
 </div>
</div>

Next, we’ll add a properties/_properties_list.ljson file to the project (make sure the name’s file and location is exactly this). This file is used by the server to render the properties as liquid objects, along with any variables we define:

properties :
{% for property in properties %}
  -
    photo : {{ property.main_photo | url_for_property_photo : "300x185" }}
    property_url : {{ property | url_for_property }}
    short_description : {{ property.short_description | truncate : 150 | capitalize | yaml_safe}}
    display_address : {{property.display_address | truncate: 90 | yaml_safe}}
    price : {{ property.price }}
    bedrooms: {{property.bedrooms | yaml_safe }}
    property_id: {{property.property_id}}
    status: {{ property.status }}
    primary_channel: {{property.primary_channel}}
{% endfor %}
pagination :
    current_page : {{ pagination.current_page }}
    has_prev_page : {{ pagination.has_prev_page }}
    total_count : {{ pagination.total_count }}
    has_next_page : {{ pagination.has_next_page }}
    from_record : {{ pagination.from_record }}
    to_record : {{ pagination.to_record}}

You can see a comprehensive list of the available variables for a property here.

Now we can define a template that will be used by each infinite scroll page. As with all HTML template script elements, this can be placed anywhere as long as it’s available somewhere in the DOM on the page where it needs to be rendered. This template can make use of the variables we defined inside the _properties_list.ljson file:

{% raw %}
 <script id="infinite_scroll_properties_template" type="text/liquid">
  <ul>
   {% for property in properties %}
    <li>
     <a href="{{ property.property_url }}">
      <img src="{{ property.photo }}">
     </a>
     {{ property.status }}
     <a href="{{ property.property_url }}">
      {{ property.price }}
      {{ property.bedrooms }} bedrooms
     </a>
     <a class="btn-primary" href="{{ property.property_url }}">Full Details</a>
    </li>
   {% endfor %}
  </ul>
 </script>
{% endraw %}

Note that the template is wrapped in raw tags, so the serverside parser will ignore it. The consequence of this is that you cannot use complex liquid statements, includes for partials or JavaScript inside it. This means that things like liquid filters should be used inside the ljson file, where variables are defined. It is often best to keep the infinite scroll template entirely separate from the normal property template, as it will need to be structured differently for it to be compiled by the JavaScript Liquid engine. Also note that this structure will be a direct copy of the structure within your properties for loop.

This should be enough to get the infinite scroll working on your property index page. As always, you might need to tweak the containing elements and CSS as required to get it to format well.

Infinite scroll options

Overlays

The function also ships with a loading overlay should you want to use it. It is a simple fullscreen fade in and out whilst the properties are being fetched. If you’d rather not have it displayed, you can simply place it far off screen so even when it’s triggered it cannot be seen:

$('.loading').css('left', '9999px')


Load more on click

If you would prefer to load results on a button press, add the config below:

Ctesius.addConfig('autoscroll_infinite_scroll', false);

Next, add your anchor or button and provide it with an onclick kick event:

<button type="button" onclick="Ctesius.kickEvent('load_next_for_infinite_scroll');">
 Load more properties
</button>


Hiding the button

When there is no next page to load, you will want to hide the load more button. To do this, add the following event:

Ctesius.registerEvent('performed_next_infinite_scroll', function(a){
  if (!a.pagination.has_next_page){
    $('.your_button_class').hide();
  }
});