
Sitecore JSS supports code-first and Sitecore-first workflows, but sometimes this doesn't fit everyone's needs. These workflows can force frontend and backend developers to use a workflow they don't want to. For example a frontender wants to create a React app and have the freedom to decide what the data structure should look like. And a backender wants to create a viewmodel without limitations.
With the contract-first workflow the frontend and backend decide together what the data structure should look like. The frontender can use tools like Storybook to create their React components and the backender will make sure their viewmodel matches what the component expects. So the data structure becomes the 'contract'.
This workflow has similarities to the Sitecore-first workflow, but the backender will make a custom Rendering Contents Resolver in which the Sitecore item is transformed to the viewmodel which matches the data structure contract.
This example will create a promo block with the contract-first workflow. First the frontend and backend developer will make the contract:
Data.json
{
"image": {
"src": "http://placehold.it/750x422/333333/888888?w=750&text=750fblx",
"alt": "alt text",
"srcSet": [
{
"src": "http://placehold.it/750x422/333333/888888?w=750&text=750lx",
"width": 750
},
{
"src": "http://placehold.it/1536x1200/333333/888888?w=1536&text=1536lx",
"width": 1536
},
{
"src": "http://placehold.it/1920x600/333333/888888?w=1920&text=1920l",
"width": 1920
}
]
},
"richText": {
"value": "<p>Lorem ipsum dolor sit amet, consectetur adipisicing, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>"
},
"heading": {
"text": {
"value": "Do you have any questions?"
},
"level": 2
},
"button": {
"value": {
"text": "Frequently asked questions",
"href": "/#/"
}
}
}
Based on this contract the frontender can create the React component with Storybook:
Promo.jsx
const PromoBlock = (props) => {
const {
image,
richText,
heading,
button,
} = props;
return (
<Theme>
<Layer>
<Retain>
<div className="c-promoblock">
<div className="o-layout o-layout--gutter">
<div className="o-layout__cell u-fraction--1/2@from-lap">
<Img image={image} className="u-m-b" />
</div>
<div className="o-layout__cell u-fraction--1/2@from-lap">
<Heading text={heading.text} level={heading.level} className="u-m-t-tiny" />
<Rte richText={richText} />
{button ? (
<Button tag="button" modifier="secondary" field={button} />
) : null}
</div>
</div>
</div>
</Retain>
</Layer>
</Theme>
);
};
At the same time the backender can start with the template in Sitecore. After that he can create the custom Rendering Contents Resolver and the mapper to create the viewmodel.
PromoRenderingContentsResolver.cs
public class PromoRenderingContentsResolver : IRenderingContentsResolver
{
private readonly Func<IMvcContext> contextThunk;
private readonly IMapper mapper;
public PromoRenderingContentsResolver(Func<IMvcContext> contextThunk, IMapper mapper)
{
this.contextThunk = contextThunk;
this.mapper = mapper;
}
public override object ResolveContents(Rendering rendering, IRenderingConfiguration renderingConfig)
{
var context = this.contextThunk();
var datasource = context.GetDataSourceItem<Promo>();
var model = this.mapper.Map<PromoJsonDto>(datasource);
return model;
}
}
PromoToPromoJsonDtoTypeConverter.cs
public class PromoToPromoJsonDtoTypeConverter : ITypeConverter<Promo, PromoJsonDto>
{
private readonly Func<IGlassHtml> htmlThunk;
private readonly IMapper mapper;
public PromoToPromoJsonDtoTypeConverter(Func<IGlassHtml> htmlThunk, IMapper mapper)
{
this.htmlThunk = htmlThunk ?? throw new ArgumentNullException(nameof(htmlThunk));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public PromoJsonDto Convert(Promo source, PromoJsonDto destination, ResolutionContext context)
{
if (source == null)
{
return destination;
}
var result = destination ?? new PromoJsonDto();
var html = this.htmlThunk();
var mediaOptions = new MediaOptions(750, 1536, 1920);
result.Image = this.mapper.Map<ImageJsonDto>(
source.Image,
opt => { opt.Items[MediaConstants.MediaOptions] = mediaOptions; });
result.RichText = new FieldValueJsonDto { Value = html.Editable(source, x => x.Text) };
result.Button = this.mapper.Map<LinkJsonDto>(source.Button);
result.Heading = new HeadingJsonDto
{
Level = 2,
Text = new FieldValueJsonDto { Value = html.Editable(source, x => x.Heading) }
};
return result;
}
}
The frontender and backender can work in parallel because they can both work with the same data structure. This also makes a clean separation of responsibilities.
Using a custom Rendering Contents Resolver has some limitations. Values from a custom IRenderingContentsResolver implementation cannot be edited in the Experience Editor. However if you look at the PromoToPromoJsonDtoTypeConverter.cs example you'll see that there is a FieldValueJsonDto which has html.Editable from Glass Mapper. So the output required for the Experience Editor can still be used like this. These values are still editable in the Experience Editor.
Instead of a custom Rendering Contents Resolver it's probably also possible to do this with GraphQL, but than the logic would move from the backend to the frontend and this way it's also much easier to integrate 3rd party data. The logic which first was done in a controller is now being done in the custom Rendering Contents Resolver. Not everything you could do in a controller works, but for a backender they will be pretty similar.
The datasource item in PromoRenderingContentsResolver.cs is a strongly typed Glass Mapper model which is generated with Leprechaun. More info can be found here.
I've used contract-first in combination with Helix at a couple of project so far and this has become my preferred workflow. I also gave a presentation about this subject at SUGNL. You can find the slides here.