Resources

So far our example custom coded components and actions had very simple property types: select, boolean, string, etc. But what if your property is a data object like a product, collection or a CMS entry? At Shopstory we call these objects "resources" and you can create as many resource types as you need. They can be connected to any data source (e-commerce platform, CMS, etc) and you're in full control over how data is fetched by your front-end code.

Custom component with resource property

Let's build a simple ProductCard component that takes 2 properties:

  1. product -> the object with product data like title, description, price, etc. fetched from a mock e-commerce platform service. This property is our resource.

  2. hasOverlay -> simple boolean prop controlling appearance

The code of our ProductCard looks like this:

import React from "react";
import {Product} from "../../products/MockProductsService";
import Image from 'next/image'

export const ProductCard : React.FC<{ product: Product, hasOverlay: boolean }> = ({ product, hasOverlay }) => {
  return <div style={{position: "relative"}}>
    <div style={{position: "relative"}}>
      <div style={{paddingBottom: "66.666%", background: hasOverlay ? "#f3f3f3" : "none"}}>
        <Image src={product.image} alt={product.title} layout={"fill"} style={{position: "absolute", top: 0, left: 0, bottom: 0, right: 0}}/>
      </div>
    </div>
    <div style={{paddingBottom: 6, paddingTop: 20, textAlign: "center", fontSize: 15}}>
      { product.title }
    </div>
    <div style={{fontWeight: 600, textAlign: "center", fontSize: 15}}>
      { product.price }$
    </div>
  </div>
}

Schema - resource field

In order to register ProductCard in Shopstory and take into account product property we must use resource field type:

export const shopstoryConfig: Config = {
    // ...
  components: [
    {
      id: "ProductCard",
      label: "Product Card",
      type: "card",
      schema: [
        {
          prop: "hasOverlay",
          label: "Has overlay",
          type: "boolean"
        },
        {
          prop: "product",
          label: "Product",
          type: "resource", // resource field
          resourceType: "product" // resource type
        }
      ]
    }
  ],
}

Each resource field must always define resourceType which in our case is product.

At this point Shopstory has no knowledge of the resource called product. When you open Shopstory and add ProductCard the editor will crash saying that product widget is not defined.

Registering resource type

Each resource type in Shopstory consists of 2 parts:

  • widget

  • fetch function

Widget

Widget defines how resources are browsed and picked by the user in the Shopstory editor. Widget code is run only inside the Shopstory editor and is not involved when you render Shopstory content in your website.

Let's create a widget for our product resource type. We do this via resourceTypes property in the Shopstory configuration object:

export const shopstoryConfig: Config = {
    // ...
  resourceTypes: {
    product: {
      widget: {
        type: "item-picker",
        getItems: async (query) => {
          const products = await MockProductsService.searchProducts(query);
          return products.map(product => ({
            id: product.id,
            title: product.title,
            thumbnail: product.image
          }))
        },
        getItemById: async (id) => {
          const product = await MockProductsService.getProductById(id);
          if (!product) {
            throw new Error("can't find product");
          }

          return {
            id: product.id,
            title: product.title,
            thumbnail: product.image
          }
        }
      }
    }
  },
}

In this code we tell Shopstory that product should use a built-in item-picker widget. Item picker is a simple single-select widget that requires 2 functions to work properly:

  • getItems - searches for items by query

  • getItemById - standard find-by-id function

After the widget is defined you can add ProductCard to the Shopstory canvas and use item picker to pick a product:

As you can see the widget works properly but after selecting the product, the component still shows a placeholder. It's because we didn't define a second important part of the resource type: a fetch function.

Fetch function

When user picks a resource the only data saved in Shopstory data is resource identifier. In our example it's the product id - the id property returned by getItems and getItemById functions. Shopstory doesn't keep any data copies (like product title, description, price, etc) inside of its content so there is no problem with synchronising the data. It also means that if you want to render some content, Shopstory must first first fetch all the resources used by that content. How data is fetched is controlled by so called "fetch functions".

In our example we haven't defined a fetch function for our product so it's no surprise that the ProductCard is not rendered. Unsuccessful fetch will result in the field label turning red:

If you look at the console you'll see following error message:

To solve this we must add a fetch function to our product resource type:

export const shopstoryConfig: Config = {
    // ...
  resourceTypes: {
    product: {
      fetch: async (resources) => {
        const ids = resources.map(resource => resource.id);
        const products = await MockProductsService.getProductsByIds(ids);

        return resources.map(resource => ({
          ...resource,
          value: products.find(product => product.id === resource.id)
        }))
      },
      widget: {
        // ...widget definition
      }
    }
  },
}

The first parameter of fetch function is an array of resources that should be fetched. Each resource of this array has a following structure:

{
    id: string;
    type: string;
    fetchParams?: ResourceParams;
    info?: ResourceInfo;
}

id is the resource unique identifier. type represents resource type, in this case it's always gonna have value "product". In our example fetchParams and info are not defined, we'll discuss them later.

The result of the fetch function should be an array of fetched resources. Fetched resource is represented by the same object as input resource with one extra property - either value (successfully fetched resource data) or error.

For example for the following input:

[ 
    { id: "xxx", type: "product" },
    { id: "yyy", type: "product" },
    { id: "zzz", type: "product" }
]

The output could look like this:

[
    {
        id: "xxx",
        type: "product",
        value: {
            title: "Super xxx product",
            description: "Xxx is awesome.",
            price: "19.99",
            image: { /* image object */ }
        }
    },
    {
        id: "yyy",
        type: "product",
        value: {
            title: "Super yyy product",
            description: "Yyy is even more awesome.",
            price: "229.99",
            image: { /* image object */ }
        }
    },
    {
        id: "zzz",
        type: "product",
        error: /* error */
    }
]

The products xxx and yyy were fetched successfully and in case of zzz there was some fetching error that you intercepted and provided info about it to Shopstory.

Let's see the end result after adding the fetch function:

Errors in fetch function

If for some reason your fetch function throws an error then all resources will be treated as if they had an error property set.

Optional resources

Sometimes the resource are not required to render the component. In such case you can set the flag optional to true:

  {
    prop: "product",
    label: "Product",
    type: "resource",
    resourceType: "product",
    optional: true
  }

If the resource is not picked or fetching the resource resulted in error, the product prop for your custom component will be undefined.

Built-in resource types (connecting CMS data)

Shopstory plugins can register their own resource types. The main application of this feature is connecting CMS data (entries, media, etc) to custom components or actions.

For example, imagine you manage all your website URLs in the CMS (a common pattern when SEO is critical). You've created a content type (or document type) named UrlRoute and each website URL is represented by the entry of this type. In this scenario, when editor adds a link action to a button in Shopstory, you obviously want to connect this link to some entry of UrlRoute instead of hardcoding link in a text field. You can achieve such effect with built-in resource types.

Each Shopstory plugin can register its own resource types. CMS plugins, like contentfulPlugin or sanityPlugin always add built-in resource types for connecting to CMS-specific data.

Contentful registers 2 resource types:

  • contentful-entry (for connecting to entries)

  • contentful-asset (assets)

Sanity registers 3 resource types:

  • sanity.document (connects to other documents)

  • sanity.image (connects to images)

  • sanity.file (connects to files).

Each resource types can have its parameters. For example you can easily narrow down contentful-entry or sanity.document resource types to specific content types / document types. You can find in-depth specification of CMS-specific resource types with their parameters in CMS Guides.

Our custom link example would look like this in Contentful (transform function will be described later in this chapter):

{
      id: 'MyLink',
      label: 'URL Route',
      schema: [
        {
          params: {
            contentTypeId: 'UrlRoute' // we narrow down entries to UrlRoute content type
          },
          prop: 'link',
          resourceType: 'contentful-entry',
          transform: (link: any) => {
            return {
              path: link.fields.path
            };
          },
          type: 'resource'
        }
      ]
    },

Default fetch function

Built-in resource types can come with a default fetch function. It means that if you don't define your own fetch function they'll be fetched in a default way. The default fetch behaviour depends on the CMS, you learn how it works in CMS Guides.

transform property

If you want to keep the default fetch but make some transformation of the result data, you can do it using transform property. In the above-mentioned custom link example, only the { path: link.fields.path } will be sent to the link component.

Custom fetch

In many cases you will want to completely ignore default fetch behaviour and override it with your own data fetching. Built-in resource types are no different than custom resource types and you can easily do it with providing your own fetch function.

In the example below we override how sanity.document data is fetched:

export const shopstoryConfig: Config = {
  // ...
  resourceTypes: {
    // ...
    "sanity.document": {
      fetch: async (resources) => {
        return await fetchSanityDocumentResources(resources); // custom fetch function
      },
    },
  }
};

Your custom fetch function doesn't have to fetch all the resources from resources input array. The resource is considered fetched when its value or error properties are set. For resources where value and error stay undefined the custom fetcher will be applied after custom fetch finished running.

Last updated