App Lib v2 Customization

A guide for customizing our app lib.

Important: Please read: App Lib v2 Structure before reading this.

1. Overview

We do customization in boost-pfs-filter.js or boost-pfs-instant-search.js file, by overriding components functions.

In the function we override, this points to the component, and you can use this to access the data of that component.

There are 3 ways to override functions:

  • Override before/after functions
  • Override special functions
  • Override other functions
Important: Custom code need to be written with ES5 syntax.

1.1. Override before/after function

These functions are on all components, they're all empty functions that you can override:

beforeInit, afterInit, beforeRender, afterRender, beforeBindEvents, afterBindEvents.

Recommend using before/after function instead of init, render, and bindEvents itself.

When to override which function:

  • before/afterInit:
    • To create new component (example: this.customButton = new ApplyButton() )
    • To change child's component position → see section 2.1.1
  • before/afterRender:
    • To render new component (example: this.$element.append(this.customButton.$element)
    • To add/remove css classes
  • before/afterBindEvents:
    • To add extra events
    • To remove events (example: jQ(this.$element).off('click'))

Example: Add a css class to filter options

FilterOption.prototype.afterRender = function() {
	// "this" points to the filter option component
	this.$element.addClass('my-custom-css-class');
}

Example: Disable click event on filter option item

FilterOptionItem.prototype.afterBindEvents = function() {
	this.$element.off('click');
}

Example: Display number of selected filter option item

// We use the afterRender function of FilterTree, it will be called after the filter tree is rendered
FilterTree.prototype.afterRender = function() {
	var count = 0;


	this.filterOptions.forEach(function(filterOption) {
		// FilterOption has numberAppliedFilterItems field. We add those up.
		count += filterOption.numberAppliedFilterItems;
	})


	// We prepend the count number above the filter tree
	jQ('').prepend('<div> Selected filters: ' + count + '</div>');
}
Important: To quickly list all available fields/functions for each component, type boostPFS in the browser console for the whole component tree.

1.2. Override other functions in app lib

Somtimes the before/after functions and special functions are not enough.

In these cases, we override function from app lib directly.

Important: in app lib v1, we copy the whole function out, but for app lib v2, please use this new way
var originalFunction = Component.prototype.functionName;
Component.prototype.functionName = function(param1, param2) {
	// Call the original function in our app lib.
	// We don't have to copy a very big function out here to modify.
	// function.call(this, param1, param2) binds the "this" pointer and params to the original function.
	originalFunction.call(this, param1, param2);


	// Do your custom code after the original function has run
}

Example: See section 2.4.

2. App lib customization

2.1. Commonly asked custom elements (clear all, apply all,...)

Commonly requested elements are placed in the filter tree, but display none by default.

We have a header and footer sections on filter tree, to store these elements. It's like a toolbar to get custom elements.

On filter tree (both vertical and horizontal), there are header/footer sections with:

  • Apply all button (in footer)
  • Clear all button (in header)
  • Hide filter button (in header)

On filter option (both vertical and horizontal):

  • Number of selected filter items (on title)
    • Can also be accessed in filterOption.numberAppliedFilterItems
  • Apply button (on each filter option)

On refine by:

  • A setting to separate refine by from the filter tree, and display it as vertical/horizontal.

2.1.1. Change element's position

The parent component appends the child component to itself.

Example: In our app lib the FilterTree component render the ApplyButton on the footer

// Original code in app lib
FilterTree.prototype.render = function() {
	...
	this.$element.find(this.selector.filterFooter).append(this.applyAllButton.$element);
	...
}

We want to change the apply all button to the header: override the afterRender function

// We override this function in boost-pfs-filter.js
FilterTree.prototype.afterRender = function() {
	this.$element.find(this.selector.filterHeader).append(this.applyAllButton.$element);
}

⇒ We changed the element's position from footer to header.

Example: Place the apply all button outside of filter tree:

// We override this function in boost-pfs-filter.js
FilterTree.prototype.afterRender = function() {
	jQ('#custom-container').append(this.applyAllButton.$element);
}

Demo: Vertical Layout - Expand filter - Overlay

2.2. Render extra elements

2.2.1. Simple element

Example: render a close button on the footer

The footer is part of the filter tree, we override FilterTree's afterRender

// We override this function in boost-pfs-filter.js
FilterTree.prototype.afterRender = function() {
	// this.$element points to the filter tree on the DOM
	this.$element
		.find(this.selector.filterFooter)
		.append('<div class="custom-button">Close</div>');
}


FilterTree.prototype.afterBindEvents = function() {
	this.$element.find('.custom-button').on('click', this.onCloseFilterTree.bind(this));
	// this.onCloseFilterTree can be replaced with a function outside of app lib
}

2.2.2. Component element

Example: We want 2 apply all button, on header and on footer. Currently app lib has 1 apply all button on footer. Need to add another on header.

FilterTree.prototype.afterInit = function() {
	// creates a 2nd apply button after init
	this.applyButton2 = new ApplyButton(this.filterTreeType, 'apply-all');
	this.addComponent(this.applyButton2);
}


FilterTree.prototype.afterRender = function() {
	// append the 2nd apply button to filter header
	this.$element
		.find(this.selector.filterHeader)
		.append(this.applyButton2.$element);
}

2.3. Modify events

We use the afterBindEvents function, to unbind original event, and insert our own events

Example: On filter by multi-level tag, customize for the first level acts like a label for toggling, not like a filterable tag

FilterOptionItemMultiLevelTag.prototype.afterBindEvents = function() {
	if (this.level == 1) {
		this.$element.off('click');
		this.$element.on('click', yourCustomFunction.bind(this));
	}
}


function yourCustomFunction() {
	this.$element.find('ul').toggleClass('open');
}

2.4. Modify other function without before/after

We sometimes need to override functions outside of render/bindEvent. These functions don't have a before or after function.

We don't want to copy a big function out like in app lib v1. So we will do this:

Example: Capitalize first letter of filter item label, except for 'iPhone' and 'iPad'.

⇒ Need to change the buildLabel function

// Save the original function
var originalBuildLabelFunction = FilterOptionItem.prototype.buildLabel;


FilterOptionItem.prototype.buildLabel = function() {
	var label = originalBuildLabelFunction.call(this);


	// Do your custom code after the original function has run
	if (label == 'Iphone') label = 'iPhone';
	if (label == 'Ipad') label = 'iPad';
	
	return label;
}

2.5. Commonly customized functions

2.5.1. Show/hide filter tree

  • Click a button outside the filter, to show/hide filter
  • Click a button inside the filter, to hide filter

The show/hide button is the mobile button, change css to show on desktop.

Function to handle clicking mobile button:

FilterMobileButton.prototype.onClickMobileButton

Function to handle clicking the close button inside filter tree:

FilterTree.prototype.onCloseFilterTree

2.5.2. Display refine by separately

Enable settings in boost-pfs-filter.js

var boostPFSFilterConfig = {
  general: {
    separateRefineByFromFilter: true
  }
}

Place this div wherever you want to render refine by

  • Vertical refine by
<div class='boost-pfs-filter-refine-by-wrapper-v'></div>
  • Horizontal refine by
<div class='boost-pfs-filter-refine-by-wrapper-h'></div>

2.6. Customize HTML template (Advanced)

Override the functions

  • getTemplate, compileTemplate, render

Get template returns the raw template string, before replacing anything

Example: FilterTree.getTemplate() returns a string:

<div class="boost-pfs-filter-tree-content" aria-live="polite" role="navigation" aria-label="{{label.productFilter}}">
	{{header}}
	<div class="{{class.filterRefineByWrapper}}">
		{{refineBy}}
	</div>
	<div class="{{class.filterOptionsWrapper}}">
		{{filterOptions}}
	</div>
	{{footer}}
</div>
Important:
  • You can change the html by overriding getTemplate, with the condition that the in render function, children element must be appended correctly.
  • In the above example, {{refineBy}} and {{filterOptions}} are components
  • The string {{refineBy}} and {{filterOptions}} will be replaced with empty string. Showing it in the template is only for code-reading purpose.
  • refineBy and filterOption elements will be appended to the parent element in the render function,
    // In the compile template function
    FilterTree.prototype.compileTemplate = function() {
    	....
    	// We get the raw template
    	this.getTemplate()
    		// Replace refine by with empty string
    		.replace({{refineBy}}, '');
    	....
    }
    // In the render function
    FilterTree.prototype.render = function() {
    	....
    	// Create new jquery element from HTML string in compile template
    	this.$element = jQ(this.compileTemplate())
    	// Append the refine by element, based on selector
    	this.$element.find(this.selector.refineByContainer).append(this.refineBy.$element);
    	....
    }
    		

Why we do this: each element is its own object with events, data, functions... and is not a string. So we can't replace {{refineBy}} with a html string like in app lib v1.


3. Access data returned from API

A copy of the original data returned from API is kept in boostPFS.filter.data. You can access this field anytime anywhere.

3.1. Customize filter value sorting

Function to use:

FilterOption.prototype.sortValues(values)

This function need to modify the values array in-place.

Example: sort the size values by your custom rules

var originalSortFn = FilterOption.prototype.sortValues;
FilterOption.prototype.sortValues = function(values){
	// If this is not the size filter option, call the original sort function
	if (this.filterOptionId != 'pf_otp_size') {
		originalSortFn.call(this, values);
	// Run your custom sort
	} else {
		sortingArr = ['S', 'M', 'L'];


		// This sort function runs in-place, modify the values array directly
		values.sort(function(a, b){  
		  return sortingArr.indexOf(a) - sortingArr.indexOf(b);
		});
	}
	// Here we don't need to return anything.
}

4. Theme customization

4.1. Functions to override for theme elements

Override compileTemplate function to return HTML string

The bindEvents function is optional.

Important: Use this.$element, this.data,... to access the component's data for rendering.
  • Product Items:
ProductGridItem.prototype.compileTemplate()
ProductListItem.prototype.compileTemplate()
ProductCollageItem.prototype.compileTemplate()
  • Pagination
ProductPaginationDefault.prototype.compileTemplate()
ProductPaginationDefault.prototype.bindEvents()
  • Other elements:
Breadcrumb.prototype.compileTemplate()


ProductDisplayType.prototype.compileTemplate()
	ProductDisplayType.prototype.bindEvents()


ProductLimit.prototype.compileTemplate()
ProductLimit.prototype.bindEvents()


ProductSorting.prototype.compileTemplate()
ProductSorting.prototype.bindEvents()

4.2. Functions for extra elements

buildExtraProductList in app lib v1, is in app lib v2:

// This function is called whenever re-render product list (won't call on first load)
ProductList.prototype.afterRender = function(){};

buildAdditionalElements in app lib v2, is in app lib v2:

// This function is like the above function, but is called on first load
Filter.prototype.afterRender = function(){};

5. Third-party library customization

5.1. Rangeslider

We use noUISlider for range slider.

Function to override to change slider config:

FilterOptionRangeSlider.prototype.getSliderConfig()

Returns a config object that matches this documentation: https://refreshless.com/nouislider/slider-options/

5.2. Scrollbar

We only use browser's CSS to style scrollbar.

However, if you want to customize scrollbar styles, we recommend SimpleBar. It works with any browser native scrollbar.

Documentation: https://github.com/Grsmto/simplebar/tree/master/packages/simplebar#1-documentation