Shopify Collection Filtering

One of the most confusing parts of creating a Shopify store is filtering and sorting products. Today I’d like to show you how I filter my products in Shopify. Shopify’s domain language for a group of products is called a collection. I’ll refer to this feature we are working on as a collection for the rest of the post.

Note: Shopify has a built-in way to query products using URL params and tags. If you have a very simple collection and only have a few small filters this could be a better approach. If you need more control over the filters and don’t want to re-fetch products or possibly you have a more headless approach my filter might be a better option. For a great explanation of Shopify tag filtering see christopherdodd.

Back to business. Say you have a collection of the timeless product, the T-shirt, your business requirements are to filter these shirts by color. So, you write a little filter like this when the user clicks a red swatch.



const filterByColor = color => {

  this.products.filter( product => {
    // [0] index in this case is the 'color' options
    const colors = product.options[0].values 

    return colors.indexOf(color) != -1
  })
} 


A couple of days later your project manager says “Shouldn’t we filter by size as well” So you think to yourself ok so now I need to make a filter so that I can find a “red” / “small” T-shirt. You assume the logical approach and filter products down by having to be red AND small. 

You are proud of your work but before the end of the day you get pulled aside by your project manager and she says you need to add yet another filter. “We need to filter by material now too, and while you are at it, let's filter by different color combinations”. This filter is beginning to get complex and you are also thinking now how many more filters are they going to add. Can’t we find a way to add filters more easily?



Create the Mega Filter



The MegaFilter class accepts the product array and a specification parameter. I will shed more light on the specification parameter in the coming steps. But basically, this little bit of code orchestrates the entire filter. 



export class MegaFilter {
  /**
   * @param {Product} products 
   * @param {Spec} spec 
   */
  filter(products, spec) {
    return products.filter((product) => spec.isSatisfied(product))
  }
}

Create the "Or" Specification

The OrSpecification class accepts custom specifications we will create in the next step. This class loops through all of the specifications it has been given and checks if at least one of the specifications is met on a product. For example, if we made a specification that the shirt needed to be blue and another one that needed it to be red and fed both of those specifications to the OrSpecification, then it would filter products that are blue or red.

/**
 * 'OR' specification example blue 'OR' white will satisfy, this
 * is used as a relationship between filter values in a specific specification.
 */
export class OrSpecification {
  /** 
   * @param  {Specs} specs 
   */
  constructor(...specs) {
    this.specs = specs
  }

  isSatisfied(product) {
    return this.specs.some((spec) => spec.isSatisfied(product))
  }
}

Create the "And" Specification

The AndSpecification class also accepts custom specifications. It works exactly like the OrSpecification but instead of checking if only one specification was met, it makes sure all specifications are met for the product to pass.

/**
 * 'And' specification example blue 'AND' small will only satifisy,
 * this is used as a relationship between two specification blocks.
 */
export class AndSpecification {
  /** 
   * @param  {Specs} specs 
   */
  constructor(...specs) {
    this.specs = specs
  }

  isSatisfied(product) {
    return this.specs.every((spec) => spec.isSatisfied(product))
  }
}

Create the Custom Specifications

Now to the fun part! This is where we will start writing actual filters. These are the specifications that we will pass into the “and” and “or” specification classes. Each of these classes will accept different parameters depending on what attribute you want to compare the product data to. Here is an example of a color filter that just accepts a string for the color.

export class ColorSpecification {
  
  /**
   * @param color: String
   */
  constructor(color) {
    this.color = color
  }

 /**
  * Spec Description
  * @param {Product} product
 */
 isSatisfied(product) {
   // check is product have color here..
   
   // Color is index "0"
   const colors = product.options[0].values 
  
   return colors.indexOf(this.color) !== -1
 }}

Put It Together

Pretty easy right? So let’s put it all together. Using the orchestrating classes in the code below ( I’m going to put this all on one file for ease of use. I’m also going to throw in a price specification for another example.) we can apply our custom specifications to our mega filter.

export class PriceSpecification {
  /**
   * @param {min: Number, max: Number} price
   */
  constructor(price) {
    this.min = price.min
    this.max = price.max
  }

  /**
   * Spec Description
   * @param {Product} product
   */
  isSatisfied(product) {
    const product_price = product.price / 100
    return product_price <= this.max && product_price >= this.min
  }
}


export class ColorSpecification {
  
  /**
   * @param color: String
   */
  constructor(color) {
    this.color = color
  }

 /**
  * Spec Description
  * @param {Product} product
 */
 isSatisfied(product) {
   // check is product have color here..
    
   // Color is index "0"
   const colors = product.options[0].values 
  
   return colors.indexOf(this.color) !== -1 
 }}



/**
 * 'And' specification example blue 'AND' small will only satifisy,
 * this is used as a relationship between two specification blocks.
 */
export class AndSpecification {
  /** 
   * @param  {Specs} specs 
   */
  constructor(...specs) {
    this.specs = specs
  }

  isSatisfied(product) {
    return this.specs.every((spec) => spec.isSatisfied(product))
  }
}


/**
 * 'OR' specification example blue 'OR' white will satifisy, this
 * is used as a relationship between filter values in a specific specification.
 */
export class OrSpecification {
  /** 
   * @param  {Specs} specs 
   */
  constructor(...specs) {
    this.specs = specs
  }

  isSatisfied(product) {
    return this.specs.some((spec) => spec.isSatisfied(product))
  }
}


export class MegaFilter {
  /**
   * @param {Product} products 
   * @param {Spec} spec 
   */
  filter(products, spec) {
    return products.filter((product) => spec.isSatisfied(product))
  }
}


//**********
// USE CASE
//**********

const filterProducts = () => {
  // Create Mega Filter
  const megaFilter = new MegaFilter()

  // Create Price Specificaiton
  const priceParams = { min: 0, max: 40}
  const price_spec =  new PriceSpecification(priceParams)
      
  // Create Color Specifications
  const color_spec = ['blue','red'].map((value) => new ColorSpecification(value))

  // Group Specs
  const colorOptions = new OrSpecification(...color_spec)   
  // Note: This will effectively create a filter that finds products that has atlease one of the colors AND
  // is within the price limitations expressed in the price filter. 
  const finalSpec = new AndSpecification(price_spec, colorOptions)

  // Filter products ** this.products would be your products array **
  const filteredProducts = megaFilter.filter(this.products, finalSpec)
      
}

I'm sure you have guessed it by now but this pattern could be used to filter really anything else you wanted. I used this pattern just the other day to filter stores on a map for a store locator ( I'll post more on that later).

That's all for now, stay learning.