In previous blog I described how to setup SXA Grid System with Sitecore JSS. In this post we’ll learn how to implement SXA Column Splitter component in JSS.
In standard SXA this component allows to easily split content into manageable columns. With some changes to Layout Service we can achieve similar behavior in Sitecore JSS:
Source code described in this blog is available on Github.
Items configuration
Before we start we need to configure grid system for our SXA site following SXA Grid System with Sitecore JSS. We only need to configure site settings item, select grid mapping and optionally for bootstrap to update Grid Definition item. Changes to components, or pipelines described in that post are not necessary.
Next we need new template for Rendering Parameters inheriting from /sitecore/templates/Feature/Experience Accelerator/Page Structure/Rendering Parameters/ColumnSplitter
. This will give the ability to configure size of each column in the splitter:
Optionally we can add _Standard Values
item for this template, with default column splitter values (e.g. two column split on desktop and one column on mobile). Hint: you can edit Columns field using Raw values in format: {item guid}|{item guid}
.
Next we need to create new Placeholder Settings
item for placeholder column
and assign renderings which you want to insert to the columns to Allowed Controls
field:
We can now add new Json Rendering item for our Column Splitter component pointing to JSS implementation:
In this item we would need to:
- Select template created in first step in
Parameter Template
field:
- Select
Add Column
andRemove Column
inExperience Editor Buttons
field:
- Add
column
placeholder toLayout Service Placeholders
field:
Now we need to allow to add our new component into the layout, we do it by adding Column Splitter
component to Allowed Controls
of your main placeholder settings items.
We can add now column splitter to the layout and check how Layout Service returns it. If you setup two columns in rendering parameters Standard Values item for Desktop and one column on Mobile you should have something similar to this:
We need to solve two issues here:
- Change field
ColumnWith{column}
in params from items guids to grid system CSS classes, which can be used easily in JSS. - Inform JSS component which columns are actually selected by Content Author (we can use
EnabledPlaceholders
field from params) and return components added to thecolumn
placeholder in proper way to allow assigning different content to each column.
Pipelines Code
To solve first issue we need to add our custom processor to renderJsonRendering
pipeline, which will read grid definition items and replace IDs with class names in output JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
namespace SmartSitecore.Feature.ColumnSplitter.Pipelines { using System.Collections.Generic; using System.Linq; using Sitecore.Diagnostics; using Sitecore.LayoutService.Configuration; using Sitecore.LayoutService.ItemRendering; using Sitecore.LayoutService.Presentation.Pipelines.RenderJsonRendering; /// <summary> /// Handle SXA grid rendering parameters in Sitecore Layout Services. /// </summary> public class ColumnSplitterRenderingParametersProcessor : BaseRenderJsonRendering { private const string GridParamsKey = "ColumnWidth"; private const string GridCssKey = "Class"; /// <summary> /// Initializes a new instance of the <see cref="ColumnSplitterRenderingParametersProcessor"/> class. /// </summary> /// <param name="configuration">IConfiguration object parameter.</param> public ColumnSplitterRenderingParametersProcessor(IConfiguration configuration) : base(configuration) { } /// <inheritdoc/> protected override void SetResult(RenderJsonRenderingArgs args) { Assert.ArgumentNotNull(args, nameof(args)); Assert.IsNotNull(args.Result, "args.Result should not be null"); var rendering = new RenderedJsonRendering(args.Result); if (rendering.RenderingParams == null) { return; } if (!rendering.RenderingParams.Keys.Any(o => o.StartsWith(GridParamsKey))) { return; } var db = args.Rendering?.Item?.Database; if (db == null) { return; } for (int i = 1; i < 9; i++) { if (!rendering.RenderingParams.ContainsKey(GridParamsKey + i)) { break; } var gridParameters = rendering.RenderingParams[GridParamsKey + i]; var cssClasses = new List<string>(); foreach (var id in Sitecore.MainUtil.Split(gridParameters, "|")) { var item = db.GetItem(id); if (item == null) { continue; } var css = item[GridCssKey]; if (!string.IsNullOrWhiteSpace(css)) { cssClasses.Add(css); } } if (cssClasses.Any()) { rendering.RenderingParams[GridParamsKey + i] = string.Join(" ", cssClasses); } } args.Result = rendering; } } } |
We need to call this processor after pipeline is initialized, with following config patch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> <sitecore role:require="Standalone or ContentManagement or ContentDelivery"> <pipelines> <group groupName="layoutService"> <pipelines> <renderJsonRendering> <processor type="SmartSitecore.Feature.ColumnSplitter.Pipelines.ColumnSplitterRenderingParametersProcessor, SmartSitecore.Feature.ColumnSplitter" resolve="true" patch:after="processor[@type='Sitecore.LayoutService.Presentation.Pipelines.RenderJsonRendering.Initialize, Sitecore.LayoutService']" /> </renderJsonRendering> </pipelines> </group> </pipelines> </sitecore> </configuration> |
If we call layout service again, ColumnWith{column} params will be replaced by proper CSS classes.
To solve second issue, we need to first understand the problem which we are challenging. You probably know that JSS and Layout service uses Sitecore Dynamic Placeholders feature, but it doesn’t support it fully. You may notice that every component added into single rendering’s nested placeholder have the same dynamic placeholder key, eg.: /jss-main/column-{guid}-0. It’s because Layout Service doesn’t support dynamic placeholders within one rendering (in other words the suffix within rendering is not unique and it’s always -0).
This would be problematic for our Column Splitter, which in SXA is implemented as a single rendering which generates multiple placeholders inside. Because placeholders are not unique, if we would add a component into one column, it would be rendered multiple times in every column. We can address this issue by replacing one resolver and one pipeline processor in Layout Service:
Replace default placeholderResolver
configured in Layout Service for JSS with our custom implementation, which returns placeholders count, based on EnabledPlaceholders
field from rendering parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace SmartSitecore.Feature.ColumnSplitter.Pipelines { using Sitecore.LayoutService.Placeholders; using Sitecore.Mvc.Presentation; /// <summary> /// Resolve placeholder with sufix within rendering. /// </summary> public class ColumnSplitterPlaceholdersResolver : DynamicPlaceholdersResolver { /// <summary> /// Rendering parameters field name. /// </summary> protected const string SXAPlaceholdersFieldName = "EnabledPlaceholders"; /// <inheritdoc/> protected override int GetPlaceholderCount(Rendering ownerRendering, PlaceholderItem placeholderItem) { if (ownerRendering.Parameters.Contains(SXAPlaceholdersFieldName)) { return ownerRendering.Parameters[SXAPlaceholdersFieldName].Split(',').Length; } return 1; } } } |
Replace RenderPlaceholder
processor from renderJsonRendering
pipeline with custom implementation, which split placeholders in output JSON into different nodes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
namespace SmartSitecore.Feature.ColumnSplitter.Pipelines { using System.Collections.Generic; using Sitecore.LayoutService.Configuration; using Sitecore.LayoutService.ItemRendering; using Sitecore.LayoutService.Presentation.Pipelines.RenderJsonRendering; /// <summary> /// Split dynamic placeholders within one rendering into separate json nodes in LayoutService. /// </summary> public class ColumnSplitterRenderPlaceholders : RenderPlaceholders { /// <summary> /// Rendering parameters field name. /// </summary> protected const string SXAPlaceholdersFieldName = "EnabledPlaceholders"; /// <summary> /// Initializes a new instance of the <see cref="ColumnSplitterRenderPlaceholders"/> class. /// </summary> /// <param name="configuration">Configuration.</param> public ColumnSplitterRenderPlaceholders(IConfiguration configuration) : base(configuration) { } /// <inheritdoc/> protected override IList<RenderedPlaceholder> GetRenderedPlaceholders(RenderJsonRenderingArgs args) { var placeholders = base.GetRenderedPlaceholders(args); if (args.Rendering.Parameters != null && args.Rendering.Parameters.Contains(SXAPlaceholdersFieldName)) { return this.SplitPlaceholders(placeholders); } return placeholders; } /// <summary> /// Split dynamic placeholders within one rendering into separate json nodes /// </summary> /// <param name="placeholders">Placeholders list.</param> /// <returns>Splitted placeholders list.</returns> protected virtual IList<RenderedPlaceholder> SplitPlaceholders(IList<RenderedPlaceholder> placeholders) { int i = 1; if (placeholders.Count > 1) { foreach (var pl in placeholders) { pl.Name = $"{pl.Name}-{i}"; i++; } } return placeholders; } } } |
The final config patch looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> <sitecore role:require="Standalone or ContentManagement or ContentDelivery"> <pipelines> <group groupName="layoutService"> <pipelines> <renderJsonRendering> <processor type="SmartSitecore.Feature.ColumnSplitter.Pipelines.ColumnSplitterRenderingParametersProcessor, SmartSitecore.Feature.ColumnSplitter" resolve="true" patch:after="processor[@type='Sitecore.LayoutService.Presentation.Pipelines.RenderJsonRendering.Initialize, Sitecore.LayoutService']" /> <processor type="SmartSitecore.Feature.ColumnSplitter.Pipelines.ColumnSplitterRenderPlaceholders, SmartSitecore.Feature.ColumnSplitter" resolve="true" patch:instead="processor[@type='Sitecore.LayoutService.Presentation.Pipelines.RenderJsonRendering.RenderPlaceholders, Sitecore.LayoutService']" /> </renderJsonRendering> </pipelines> </group> </pipelines> <layoutService> <configurations> <config name="jss"> <rendering> <placeholdersResolver type="SmartSitecore.Feature.ColumnSplitter.Pipelines.ColumnSplitterPlaceholdersResolver, SmartSitecore.Feature.ColumnSplitter" patch:instead="placeholdersResolver[@type='Sitecore.LayoutService.Placeholders.DynamicPlaceholdersResolver, Sitecore.LayoutService']" /> </rendering> </config> <config name="sxa-jss"> <rendering> <placeholdersResolver type="SmartSitecore.Feature.ColumnSplitter.Pipelines.ColumnSplitterPlaceholdersResolver, SmartSitecore.Feature.ColumnSplitter" patch:instead="placeholdersResolver[@type='Sitecore.LayoutService.Placeholders.DynamicPlaceholdersResolver, Sitecore.LayoutService']" /> </rendering> </config> </configurations> </layoutService> </sitecore> </configuration> |
If we would call layout service again we shall have following results:
Hint: there is alternative way of solving the issue of returning dynamic placeholders within a single rendering, which will not split placeholders into different nodes in Json, but returns all in single node one by one. Instead of changing RenderPlaceholder
processor you would need to replace Sitecore.JavaScriptServices.ViewEngine.LayoutService.Serialization.PlaceholderTransformer
(this is the place where Sitecore transforms placeholders into Json objects). This way may be more close to what Sitecore is doing by default, when returning placeholders in Layout Service, but I found it more problematic to handle in JSS, especially in Experience Editor, where Layout Service returns more elements for a placeholder. It also requires to replace some services in Layout Service dependency injection.
JSS implementation
In our component implementation in JSS we need to use EnabledPlaceholders
parameter to render as much placeholders as needed and dynamically change each placeholder name, so it will take rendering from proper Json node. React implementation would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React from 'react'; import { Placeholder } from '@sitecore-jss/sitecore-jss-react'; const ColumnSplitter = ({ rendering, params }) => { const columns = params.EnabledPlaceholders?.split(','); return ( <div className="row"> {columns?.map((value) => { return ( <div key={value} className={`col ${params[`ColumnWidth${value}`]}`}> <Placeholder name={`column-${value}`} rendering={rendering} /> </div> ); })} </div> ); }; export default ColumnSplitter; |
Finally let’s add some components to our columns, we can do it from Experience Editor. We can also change amount of columns (with plus and minus buttons) and configure it’s layout from Column Splitter Component Properties dialog. When we look at our Presentation Details after adding components, we shall have them with unique placeholder keys:
Layout Service shall return each as separate node in Json:
Hint: there is an issue in SXA 9.3 and SXA 10 related with Standard Values in rendering parameters, which are not applied in Experience Editor after adding a component to the page. Editor needs to open and close component properties dialog to apply it. For details check CS0194308 with Sitecore Support. On Sitecore 9.3 you may also encounter issue when adding components to the nested placeholders. Details and solution is described here: https://github.com/Sitecore/jss/pull/343. This issue is already addressed in Sitecore 10.