Code Monkey home page Code Monkey logo

crisp's Introduction

Crisp

Features

Crisp is a javascript library that enables advanced search and filtering capabilities for Shopify themes. It moves the filtering client-side which enables some cool stuff like

  • Filtering collections by tags, metafields, or anything else!
  • Filtering search results by tags, metafields, etc
  • Creating a searchable collection

Demo

When NOT to use Crisp

Crisp adds a lot of complexity to a Shopify collection page. In many cases it won't be necessary and could cause headaches. That said, if you are just excited to try Crisp skip down to getting started

You have a small catalog of products

It is already easy for your customers to find what they are looking for, there is no need to intimidate them with a bunch of advanced filtering options. Just Shopify's build in Search and Tag filtering should be more than enough.

You just want infinite scroll or search previews

You don't need Crisp to accomplish this. It is overkill. If this is your goal then consider writing your own templates and making the ajax requests yourself. It also makes it easier to account for some of the seo concerns

You want to filter a giant catalog of products

Crisp does its best to optimize for performance but it can only take it so far. If you are wanting to filter thousands of products without narrowing down the selection first (think /collections/all) Crisp may take too long to return results. In this case consider restructuring your "funnel" if you want to use Crisp, or use an app instead.

How it Works

Crisp is made up of two components, the client and the template. The client is is a Javascript library that allows (relatively) easy access to the filtered data. The second component is the template installed in the Shopify theme which gives the client access to raw unfiltered data.

Template

Crisp relies on the fairly well-known technique of using Liquid templates to create pages that can be used as JSON endpoints. This allows the client to load and filter data.

The template is simply a secondary template file in your theme (collection.crisp.liquid for example) that will never be seen when viewing the website normally. It starts with the line {% layout none %} which tells Shopify not to include the normal framing content of your site (header, tracking scripts, etc.). It then uses Liquid to build a JSON blob that the client will be able to recognize.

The client will be able to make fetch or ajax calls to access this JSON data. This can be done by changing the view query parameter to match the name of the template (example.com?view=crisp for example).

See the /templates folder for some pre-populated examples of templates.

Client

The client is where the "magic" happens. This is where all of the data loading, filtering, and configuration lives.

At its most basic, you ask the client to get you some, for example, products from the shoes collection. The client will load data from the template url (/collections/shoes?view=crisp), process it, and return to you to display to the user.

This gets more complicated when you ask tougher questions. If this time you want Size 9 or 10 running shoes in pink or purple things get a little more complicated under the hood but the interface you communicate with remains the same.

To get a little bit more into it, Crisp tries to find a balance between performance and resource usage while loading and filtering. This involves making some educated guesses in terms of how many shoes to load immediately and cancelling any extraneous requests made from the guesses as quickly as possible. Of course there are still cases where there is only one item that matches the filter constraints and it is the very last one, but in most cases Crisp works quite quickly.

SEO Concerns

TODO

Getting Started

Adding Templates

First up, we need to decide which template(s) you will need to install. This will depend on which features of Crisp you are planning to use. For example, SearchableCollection will require a Collection template and a Search template. If you are unsure which templates are required for a given feature see the corresponding entry in the API Documentation and it will list the required templates.

Now, knowing which templates we will be installing, head over to the /templates directory locate the example files that will need to be installed. Simply copy these files to your theme's /templates directory and we are ready to move on

You may have noticed a strange suffix on all the templates (ex. __DO-NOT-SELECT__.products). While this is by no means required, keep in mind that just like any alternate template this will show up in the Shopify Admin as an option for how to display a resource on the storefront. We never want to display this template by default so the underscore prefix ensures it gets pushed to the bottom of the list and, well, the "DO-NOT-SELECT" speaks for itself.

Shopify Admin Template Selector Box

Installing the Client

There are a number of ways to install the client depending on your bundler or toolchain. For this guide however, we will be simply adding it as a script tag.

Head over to the latest release and download crisp.js. Next, upload this to your themes /assets folder. Now we are ready to import it from the theme.

As with any dependency, it is good practice to only import resources on pages where they are required. For this example however, we will just be adding a global import in theme.liquid.

To import Crisp, add <script type="text/javascript" src="{{ 'crisp.js' | asset_url }}"> in the <head> of your theme.liquid.

Trying it out

Now, to test it out and make sure everything is working correctly we can use the developer console. Navigate to your storefront, open the developer tools (F12 generally), then head to the console tab.

We can now make sure the client has been installed correctly by running

Crisp.Version

It should print a version number corresponding to the latest release (or whichever version you are using)

Next, we can test out loading and filtering some resources. The exact code you will need to run will depend on which templates you installed by here is an example of loading products from a collection template in the console

{
  // Initialize the collection
  const collection = Crisp.Collection({
    handle: 'all',
    template: '__DO-NOT-SELECT__.products',
  });

  // Load first 10 products
  const products = await collection.get({
    number: 10,
  });

  // Print them to the console
  console.log(products);
}

API Documentation

Documentation

Table of Contents

Crisp.Collection

Installation

Make sure the collection template has been added and modified to suit your needs

Basic Usage

// Create a new instance
const collection = Crisp.Collection({
  handle: 'all', // REQUIRED
  template: '__DO-NOT-SELECT__.products', // REQUIRED
});

// Get the first 10 products
collection.get({
  number: 10,
  callback: function(response) {
    // Handle error
    if (response.error) {
      // Check if due to cancellation
      if (Crisp.isCancel(response.error)) {
        return;
      }
      // Non cancellation error
      throw error;
    }

    // Use products
    console.log(response.payload);
  }
});

Collection

Creates a collection instance

Parameters

Examples

const collection = Crisp.Collection({
  handle: 'all',
  template: '__DO-NOT-SELECT__.products',
});

Returns CollectionInstance

setHandle

Parameters

Examples

collection.setHandle('all');

setFilter

Parameters

Examples

collection.setFilter(function(product) {
  return product.tags.indexOf('no_show' === -1);
});

setOrder

Parameters

Examples

collection.setOrder('price-ascending');

clearOffset

Clears the internal offset stored by getNext

Examples

collection.clearOffset();

cancel

Manually cancel active network requests

Examples

collection.cancel();

preview

Retrieve the first options.number products in a collection. No filter support but extremely fast

Parameters

Examples

collection.preview({
  number: 10,
});

Returns Promise<(Payload | void)>

get

The most versatile option for retrieving products. Only recommended for use cases that require a large amount of customization

Parameters

  • options Object
    • options.number number
    • options.offset number (optional, default 0)
    • options.callback Callback (optional, default void)

Examples

collection.get({
  number: 10,
});
const payload = await collection.get({
  number: 10,
  offset: 10,
});

Returns Promise<(Payload | void)>

getNext

Similar to get but stores and increments the offset internally. This can be reset with calls to getNext. Recommended for infinite scroll and similar

Parameters

Examples

collection.getNext({
  number: 10,
});

Returns Promise<(Payload | void)>

CollectionOrder

Defines in what order products are returned

Type: ("default" | "manual" | "best-selling" | "title-ascending" | "title-descending" | "price-ascending" | "price-descending" | "created-ascending" | "created-descending")

Crisp.Search

Installation

Make sure the search template has been added and modified to suit your needs

Basic Usage

// Create a new instance
const search = Crisp.Search({
  query: 'apple', // REQUIRED
  template: '__DO-NOT-SELECT__', // REQUIRED
});

// Get the first 10
search.get({
  number: 10,
  callback: function(response) {
    // Handle error
    if (response.error) {
      // Check if due to cancellation
      if (Crisp.isCancel(response.error)) {
        return;
      }
      // Non cancellation error
      throw error;
    }

    // Use products
    console.log(response.payload);
  }
});

Search

Creates a search instance

Parameters

Examples

const collection = Crisp.Search({
  query: 'blue shirt',
  template: '__DO-NOT-SELECT__',
});

Returns SearchInstance

setQuery

Parameters

Examples

search.setQuery('blue shirt');

setFilter

Parameters

Examples

search.setFilter(function(object) {
  return object.type === 'product';
});

setTypes

Parameters

Examples

search.setTypes(['product']);

setExact

Parameters

Examples

search.setExact(false);

setAnd

Parameters

Examples

search.setAnd(false);

setFields

Parameters

Examples

search.setTypes(['title', 'author']);

clearOffset

Clears the internal offset stored by getNext

Examples

search.clearOffset();

cancel

Manually cancel active network requests

Examples

search.cancel();

preview

Parameters

Examples

search.preview({
  number: 10,
});

Returns Promise<(Payload | void)>

get

Parameters

  • options Object
    • options.number number
    • options.offset number (optional, default 0)
    • options.callback Callback (optional, default void)

Examples

search.get({
  number: 10,
});
const payload = await search.get({
  number: 10,
  offset: 10,
});

Returns Promise<(Payload | void)>

getNext

Parameters

Examples

search.getNext({
  number: 10,
});

Returns Promise<(Payload | void)>

SearchType

Type: ("article" | "page" | "product")

SearchField

Type: ("title" | "handle" | "body" | "vendor" | "product_type" | "tag" | "variant" | "sku" | "author")

Crisp.SearchableCollection

Installation

Make sure the collection template has been added and modified to suit your needs

Make sure the search template has been added and modified to suit your needs

If you plan to use the all collection in shopify it must have been created in the shopify admin. Otherwise nothing will be returned when searching within the all collection. The easiest conditions for the collection are price != 0 || price == 0.

Basic Usage

// Create a new instance
const collection = Crisp.SearchableCollection({
  handle: 'all', // REQUIRED
  collectionTemplate: '__DO-NOT-SELECT__.products', // REQUIRED
  searchTemplate: '__DO-NOT-SELECT__', // REQUIRED
});

// Get the first 10 products
collection.get({
  number: 10,
  callback: function(response) {
    // Handle error
    if (response.error) {
      // Check if due to cancellation
      if (Crisp.isCancel(response.error)) {
        return;
      }
      // Non cancellation error
      throw error;
    }

    // Use products
    console.log(response.payload);
  }
});

SearchableCollection

Creates a collection instance

Parameters

Examples

const collection = Crisp.SearchableCollection({
  handle: 'all',
  collectionTemplate: '__DO-NOT-SELECT__.products',
  searchTemplate: '__DO-NOT-SELECT__',
});

Returns SearchableCollectionInstance

setHandle

Parameters

Examples

collection.setHandle('all');

setQuery

Parameters

Examples

collection.setQuery('blue shirt');

setFilter

Parameters

Examples

collection.setFilter(function(product) {
  return product.tags.indexOf('no_show' === -1);
});

setOrder

Order only works while query === ''

Parameters

Examples

collection.setOrder('price-ascending');

setExact

Parameters

Examples

collection.setExact(false);

setAnd

Parameters

Examples

collection.setAnd(false);

setFields

Parameters

Examples

collection.setTypes(['title', 'author']);

clearOffset

Clears the internal offset stored by getNext

Examples

collection.clearOffset();

cancel

Manually cancel active network requests

Examples

collection.cancel();

get

Parameters

  • options Object
    • options.number number
    • options.offset number (optional, default 0)
    • options.callback Callback (optional, default void)

Examples

collection.get({
  number: 10,
});
const payload = await collection.get({
  number: 10,
  offset: 10,
});

Returns Promise<(Payload | void)>

getNext

Similar to get but stores and increments the offset internally. This can be reset with calls to getNext. Recommended for infinite scroll and similar

Parameters

Examples

collection.getNext({
  number: 10,
});

Returns Promise<(Payload | void)>

Crisp.Filter

Version

Crisp.Filter is available as of version 4.1.0

About

While using SearchableCollection I noticed that the majority of my javascript was dealing with keeping the filter ui and the filter function in sync. Everything felt a little awkward to I took some inspiration from flux and designed Crisp.Filter

Crisp.Filter allows writing filters in a declarative manner and then handles generating a filter function and firing appropriate events.

Filters

Crisp.Filter uses a tree internally to efficiently keep the filter state in sync. This "filter tree" needs to be provided at instantiation so Crisp.Filter can fire initial events and build the first filter function.

The filter tree is made up of nodes with children. For example the simplest node looks something like:

{
  name: 'my-unique-name',
}

The name must be unique to each node. To generate a tree like structure we can use the children property

{
  name: 'parent',
  children: [{
    name: 'child-1',
  }, {
    name: 'child-1',
  }],
}

This isn't particularly useful, but now we can start adding in filter functions. Note that Crisp.Filter's filters prop takes an array of nodes. The root node is handled internally.

const filter = Crisp.Filter({
  filters: [{
    name: 'red',
    filter: payload => payload.color === 'red',
  }, {
    name: 'blue',
    filter: payload => payload.color === 'blue',
    selected: true,
  }],
});

['red', 'blue', 'yellow'].filter(filter.fn());
// ['blue']

There is a lot to unpack there. First off, each node can have a filter property. This is a function that takes in the original payload (whatever you are filtering) and returns a boolean (like Array.filter).

Notably, the filter property is ignored unless selected is also set to true. We will get more into that later.

Once a Filter instance is created, .fn() can be called to create a filter function based on the current state of the internal tree. In this case only the 'blue' node's filter is selected so only 'blue' makes it through.

Hierarchy

Lets say, for example, that we want to filter based on the size or color of shirts. In this case size and color will be parent nodes and the options will be children

const filter = Crisp.Filter({
  filters: [{
    name: 'color',
    children: [{
      name: 'red',
      filter: shirt => shirt.color === 'red',
      selected: true,
    }, {
      name: 'blue',
      filter: shirt => shirt.color === 'blue',
    }],
  }, {
    name: 'size',
    children: [{
      name: 'small',
      filter: shirt => shirt.size === 'small',
      selected: true,
    }, {
      name: 'medium',
      filter: shirt => shirt.size === 'medium',
      selected: true,
    }, {
      name: 'large',
      filter: shirt => shirt.size === 'large',
    }],
  }],
});

This will now generate a function whose logic can be expressed like

(size 'small' OR 'medium') AND color 'red'

This is because the root node enforces logical AND on its children by default while all other nodes enforce logical OR. This can be overwridden by using the and property of a node or by passing and: boolean into the Filter options.

Selection

Selecting & Deselecting filters is very easy. Selection can be declared during instantiation but during runtime it is as simple as

filter.select('blue');
filter.deselect('small');

For those working with hierarchies there is also a helper for nodes with children which will deselect all (immediate) children

filter.clear('size');

Generally these methods will be called from event handlers. For example when someone clicks on a filter.

Events

Crisp.Filter instances emit events that can be subscribed to with the .on() method

filter.on('update', node => { /* DO STUFF */ });

Currently, the only event type is 'update' which fires anytime a nodes selected state changes.

This is where filter UI updates should occur.

URL Params

Crisp.Filter also makes syncing the url parameters and filters very easy. Calling filter.getQuery() returns a comma delimited string of the currently selected filter names. Conversely, providing filter.setQuery(string) with that same string (say on page reload) will select the correct filters and fire corresponding events.

Filter

Creates a new filter object

Parameters

Examples

const filter = Filter({
  global: [
    color => typeof color === 'string',
  ],
  filters: [
    {
      name: 'blue',
      filter: color => color === 'blue',
    },
    {
      name: 'red',
      filter: color => color === 'red',
      selected: true,
    },
    {
      name: 'yellow',
      filter: color => color === 'yellow',
    },
  ],
  and: false,
});

Returns FilterInstance

select

Selects a given node in the filter tree

Parameters

Examples

filter.select('blue');

Returns boolean Success

deselect

Deselects a given node in the filter tree

Parameters

Examples

filter.select('blue');

Returns boolean Success

clear

Deselects all children of a given node

Parameters

Examples

filter.clear('color');

Returns boolean Success

context

Returns the context of a given node

Parameters

Returns any context

fn

Generates a filter function based on the current state of the filter tree

Examples

[1, 2, 3].filter(filter.fn());

Returns FilterFunction

getQuery

Returns a comma delimited string of the selected filters

Examples

filter.getQuery();
// red,yellow

Returns string

setQuery

Takes a query are and select the required filters

Parameters

Examples

filter.setQuery('red,yellow');

on

The update event

Parameters

Examples

filter.on('update', ({ name, parent, selected, context }) => {
  // Update filter ui
});

FilterModel

Type: {name: string, filter: FilterFunction?, exclusive: boolean?, and: boolean?, selected: boolean?, context: any?, children: Array<FilterModel>?}

Properties

FilterEventCallback

The event callback

Type: Function

Parameters

Version

Active version of Crisp

Examples

console.log(Crisp.Version);
// 0.0.0

isCancel

A function to determine if an error is due to cancellation

Parameters

Examples

const cancelled = Crisp.isCancel(error);

Returns boolean

FilterFunction

Accepts an api object and returns whether to keep or remove it from the response

Type: function (any): boolean

Payload

An array of the requested api object. Generally based on a template

Type: Array<any>

Callback

A callback function that either contains the requested payload or an error. Remember to check if the error is due to cancellation via isCancel

Type: function ({payload: Payload?, error: Error?}): void

Parameters

  • args object
    • args.payload Payload (optional, default undefined)
    • args.error Error (optional, default undefined)

Examples

collection.get({
 number: 48,
 callback: function callback(response) {
   var payload = response.payload;
   var error = response.error;

   if (Crisp.isCancel(error)) {
     // Can usually ignore
     return;
   }

   if (error) {
     // Handle error
     return;
   }

   // Use payload
 }
});

Returns undefined

crisp's People

Contributors

hillairet avatar nismit avatar qw-in avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.