Shopstory
Search…
⌃K

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. 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. 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

Shopstory plugins can register their own resource types. For example if you use contentfulPlugin it will register two resource types:
  • contentful-entry
  • contentful-asset
They're responsible for picking an entry or asset from Contentful.
If you use contentful-entry you can specify what content types you want to pick from. Just use params.contentTypeId:
{
prop: "someEntry",
type: "resource",
resourceType: "contentful-entry",
params: {
contentTypeId: ['SomeContentTypeId', 'AnotherContentTypeId']
}
}

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. For example, contentful-entry and contentful-asset come with a default fetcher.

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. Below we have an example of custom link connected to URL Route Contentful entry:
export const shopstoryConfig: Config = {
// ...
links: [
{
id: 'MyLink',
label: 'URL Route',
schema: [
{
params: {
contentTypeId: 'UrlRoute'
},
prop: 'link',
resourceType: 'contentful-entry',
transform: (link: Entry<UrlRoute>) => {
return {
path: link.fields.path
};
},
type: 'resource'
}
]
}
],
}
In this example all links used by Shopstory content will be fetched with a default fetcher (unless you set your custom fetch function for contentful-entry which we'll describe in the next chapter). transform function takes in the result of the default fetch (the entry) and returns the content of the path field. The result is that the link prop passed to the custom link component will be a string with URL path.

Custom fetch

If you want to provide your own custom fetch for built-in resource types you can totally do it. All you need to do is to define fetch function for a contentful-entry or contentful-asset resource type.
Last modified 1mo ago