Displaying a Product List from a Search

This tutorial is based on the Scrivito Example App. We're going to define a “Product” object class and a simple overview widget for visitors to see the list of products. If you want something more advanced, check out the fully fledged blog included in the example app.

For you to benefit from this tutorial, basic knowledge of how Scrivito's content classes work and how they integrate with React is required.

Most websites offer some kind of overview, maybe an image gallery or a list of downloadable files or specific articles.

Overviews are meant to filter content by some criterion to help website visitors quickly find what they are looking for. In contrast to the result of a full-text search across all website content, however, an overview usually limits itself to specific items: news articles, contact persons, and the like – or products.

Define the Product object class

Let's assume that the minimum requirement of a product object is that it has a set of some properties that describe it: an order code, a title and a description, a tag list (for assigning categories to it), and an image. We could think of several other properties (availability, popularity, associated items, ...), but we'd like to keep it simple for now.

In case you're asking yourself why we're not considering creating a “ProductWidget” but have chosen to use CMS objects straight away: This is because we want each product to be unique, meaning that all references to a product must exhibit the same data. Thus, we need a single CMS object for each of them. Of course, nothing speaks against creating a “ProductWidget” for placing a particular product object on any page. But that's not what we're after here.

Let's define the “Product” object class, the data model we'll be working with in the overview widget: just create a folder in “src/Objs”, name it “Product”, and place the following files into it:

src/Objs/Product/ProductObjClass.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideObjClass('Product', {
  attributes: {
    title: 'string',
    description: 'html',
    orderCode: 'string',
    price: 'float',
    tags: 'stringlist',
    image: 'reference',
  },
});

The “Product” object class above defines the properties (attributes) to be available for each Product instance. The editing configuration below is for making these attributes editable in each “Product” widget’s properties and in the Content Browser (see below for how to make “Product” objects accessible in the Content Browser).

src/Objs/Product/ProductEditingConfig.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideEditingConfig('Product', {
  title: 'Product',
  description: 'A product.',

  attributes: {
    title: {
      title: 'Title',
    },
    description: {
      title: 'Description',
    },
    orderCode: {
      title: 'Order code',
      description: 'Must start with model number',
    },
    price: {
      title: 'Price in USD',
    },
    tags: {
      title: 'Tags',
    },
    image: {
      title: 'Image',
    },
  },

  properties: ['title', 'description', 'orderCode', 'price', 'tags', 'image'],
  titleForContent: obj => obj.get('title'),
  descriptionForContent: obj => obj.get('orderCode'),
  hideInSelectionDialogs: true,
});

Note that we are using hideInSelectionDialogs: true in the above configuration since we don’t want “Product” to show up in the page type selection dialog when creating a page.

Creating Product instances via the Content Browser

To enable editors to  create “Product” objects via the Content Browser, we need to add a filter to its configuration in “src/config/scrivitoContentBrowser.js”. After inserting the “Products” filter, you can select it in the Content Browser and add a “Product” by clicking the plus sign.

src/config/scrivitoContentBrowser.js
Copy
Scrivito.configureContentBrowser({
  filters: {
    _objClass: {
      options: {
        All: {
          title: 'All',
          icon: 'folder',
          query: Scrivito.Obj.all(),
          selected: true,
        },
        Products: {
          title: 'Products',
          icon: 'tag',
          field: '_objClass',
          value: 'Product',
        },
        Images: {
          title: 'Images',
          icon: 'image',
          field: '_objClass',
          value: 'Image',
        },
// ...
});

Providing the Product view

You've probably noticed that the “Product” class above isn't complemented with a React component for rendering its instances. Since we need to render them from within the “ProductListWidget” (we're going to define further down), we've placed the component into a dedicated file in the “src/Components” directory, “ProductView.js”:

src/Components/ProductView.js
Copy
import * as React from 'react';
import * as Scrivito from 'scrivito';

function ProductView({ product })  {
  return (
    <div className='row'>
      <div className='col-sm-2'><Scrivito.ImageTag content={ product } attribute='image' /></div>
      <div className='col-sm-9'>
        <Scrivito.ContentTag tag='h3' content={ product } attribute='title' />
        <Scrivito.ContentTag tag='h5' content={ product } attribute='description' />
      </div>
      <div className='col-sm-1'>
        <h3><small>$ </small>{ product.get('price') }</h3>
        <Scrivito.ContentTag tag='small' content={ product } attribute='orderCode' />
      </div>
    </div>
  );
};

export default Scrivito.connect(ProductView);

The above component renders a “Product” instance as columns of a Bootstrap row. In the Bootstrap grid layout, a row consists of exactly 12 equally sized sections that can be combined to form up to 12 columns. As you can see from the CSS classes we used, the Product view generates three columns with 2 + 9 + 1 = 12 sections.

We are connecting this functional component to Scrivito using Scrivito.connect to ensure that all product data has been loaded before it is rendered.

Define the ProductListWidget

One never knows where an overview of some kind may become useful in the future, at a later stage of the website. Thus, it's a good idea to provide the overview as a widget that editors may then place on the pages they choose.

Next to the above-mentioned advantages of using CMS objects for maintaining data objects (e.g. products), there's another truly beneficial aspect to this decision: You can use Scrivito's search functionality to restrict the contents of your overview to exactly the kind of items you want it to include. In our case of a “ProductListWidget”, we simply collect all CMS objects of the “Product” class. We've placed the code in the “src/Widgets/ProductListWidget” folder:

src/Widgets/ProductListWidget/ProductListWidgetClass.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideWidgetClass('ProductListWidget', {
  attributes: {
    bgColor: ['enum', { values: ['primary', 'secondary', 'info', 'light'] }],
  },
});
src/Widgets/ProductListWidget/ProductListWidgetComponent.js
Copy
import * as React from 'react';
import * as Scrivito from 'scrivito';
import ProductView from '../../Components/ProductView';

Scrivito.provideComponent('ProductListWidget', ({ widget }) => {
  const containerClass = `container bg-${widget.get('bgColor') || 'white'}`;
  const products = Scrivito.getClass('Product').all().order('title', 'asc');
  if (!products) {
  	return <p>No Products available. Create some using the Content Browser.</p>;
  }
  return (
    <div className={ containerClass }>
      {
        [...products].map(product => {
          return <ProductView key={ product.id } product={ product } />;
        })
      }
    </div>
  );
});
src/Widgets/ProductListWidget/ProductListWidgetEditingConfig.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideEditingConfig('ProductListWidget', {
  title: 'Product List Widget',
  properties: ['bgColor'],
  attributes: {
    bgColor: {
      title: 'Background color',
    },
  },
});

The “ProductListWidget” class is equipped with just one attribute, bgColor, for setting the background color of the list instances. The “Product” instances to display are determined using a search.

Searching for CMS objects

To find all Product instances, our component uses:

Copy
const products = Scrivito.getClass('Product').all().order('title', 'asc');

Now, why is this a search? The answer is simple if you recognize that Scrivito.getClass() returns an object class. All CMS object classes are subclasses of the Obj class to which the Obj search methods all() and where() can be applied to find all or, respectively, a subset of instances of the given class.

The result of the all() and where() methods of object classes is an ObjSearch. Basically, an ObjSearch is a search query result that can be narrowed down or ordered by applying and(), andNot(), or, respectively, order() to it. If, for example, we wanted to find only those “Product” instances to which a specific tag has been assigned, we could use: 

Copy
const products = Scrivito.getClass('Product').all()
  .and('tags', 'equals', tag)
  .order('title', 'asc');

After restricting the search result by applying the and() method combined with the desired operator (equals in the case above), the result is expanded to an array using the ES6 spread operator ([...products]). Each member is then rendered via the “ProductView” component we've defined above.

Summarized …

With Scrivito, it's easy to find content by specific properties (e.g. the object class) and have the result set displayed as what we called an “overview”. You can render the items in page views or in widgets, depending on the use case: If you want to enable editors to place the overview on any kind of page, create a widget like the “ProductListWidget” above. In all other cases, a dedicated result page does the job.

The searches Scrivito lets you perform using Obj.all() and Obj.where() return an ObjSearch that supports refinements and fetching the results in chunks, starting at an offset. This allows you to offer pagination.