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.Let's build a simple
ProductCard
component that takes 2 properties:- 1.
product
-> the object with product data liketitle
,description
,price
, etc. fetched from a mock e-commerce platform service. This property is our resource. - 2.
hasOverlay
-> simpleboolean
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>
}
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.Each resource type in Shopstory consists of 2 parts:
- widget
- fetch function
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 querygetItemById
- 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.
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:
If for some reason your
fetch
function throws an error then all resources will be treated as if they had an error
property set.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
.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']
}
}
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.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. 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