Atomic Design, Collette Style!

Do your content authors have the ability of modifying data and styling on your site at granular level? Do they have the ability to have an AB test done on the smallest piece of design like a button on your site? If this is what you are wanting to do then ATOMIC DESIGN is the way to go!

We at Collette are hard core Sitecorians!  When we speak about ATOMIC DESIGN, what we mean in Sitecore Language is ability to add minute rendering and have control over that minute rendering. Basically have many small/minute renderings which can be reused anywhere on the site, instead of a single rendering for the whole section.

For example: If we had to design a component shown below in the atomic way we would divide it into smaller pieces: A title component, button component, video component, a background slide etc. And these pieces would be backed up by datasources & reused anywhere.

Atomic Design


 

Now that we know what Atomic Design is,  lets look at the implementation and the road blocks we had to face:

  1. Nested Dynamic Placeholders  (Will allow us to add multiple renderings on top of each other)
    • Dynamic Placeholder Key Generator
    • Pipelines
  2. Performance Optimization

 

Nested Dynamic Placeholders

We all have heard about the Dynamic Placeholders (we used Kyle Heon Post for Dynamic Placeholders), but to have the ability of nested dynamic placeholders here’s what we had to do.

Dynamic Placeholder Key Generator

The nested dynamic placeholder keys are generated similar to the dynamic placeholder keys. Basically, we append an identifier i.e any character (char) and a unique current rendering id to the placeholderKey to make it unique. The code snippet is added below.

using System.Web;
using Sitecore.Mvc.Presentation;

namespace Collette.Library.CustomSitecore.Extensions
{
	public static class SitecoreHelper
	{
		public static HtmlString DynamicPlaceholder(this Sitecore.Mvc.Helpers.SitecoreHelper helper, string dynamicKey, char identifier)
		{
			var currentRenderingId = RenderingContext.Current.Rendering.UniqueId;
			string key = string.Format("{0}|{1}_{2}", dynamicKey, identifier, currentRenderingId);
			return helper.Placeholder( string.Format("{0}|{1}_{2}", dynamicKey, identifier, currentRenderingId) );
		}
	}
}

You can now call this Custom Sitecore helper function in your View as follows:
@Html.Sitecore.DynamicPlaceholder(‘Header-Contents’, ‘a’)
where ‘Header-Contents’ = PlaceholderKey
and ‘a’ = identifier

Tapping into Sitecore’s pipelines

Now lets look at the pipelines we need to make changes to for the nested dynamic placeholder keys to work.

  1. getPlaceholderRenderings – Responsible for retrieving allowed renderings for the given dynamicplaceholderkey.
  2. getChromeData – Responsible for assigning chromeData for the placeholder. Eg: Display Name,  Description etc for the given dynamicplaceholderkey.

getPlaceholderRenderings Pipeline

We patched our custom GetDynamicAllowedRenderings.cs inplace of  Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings from Sitecore.Kernel dll.

<getPlaceholderRenderings>
   <processor patch:instead="*[@type='Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel']" type="Collette.Library.CustomSitecore.Pipelines.GetPlaceholderRenderings.GetDynamicAllowedRenderings, Collette.Library"/>
</getPlaceholderRenderings>
			

We modified the regex for dynamic placeholders to  allow nesting of dynamic  and static placeholders. Basically this regex will help us extract the placeholderKey from given dynamicPlaceholderKey.
ForEg:
DynamicPlaceholderKey = /Page-Content/home-hero/Hero-Carousel-Slide|a_60ebe03e-2f39-4a50-a514-c6387a062d2a/Hero-Slide-Content|b_e781e9f2-38f5-4af1-a550-65156dfe4288
AfterRegexPlaceHolderKey = /Page-Content/home-hero/Hero-Carousel-Slide/Hero-Slide-Content

This placeholderKey is then used to retrieve the placeholderItem using Sitecore’s existing code.

 namespace Collette.Library.CustomSitecore.Pipelines.GetPlaceholderRenderings 
{
 public class GetDynamicAllowedRenderings : CustomGetAllowedRenderings
 {
 public new void Process(GetPlaceholderRenderingsArgs args)
 {
 Assert.IsNotNull(args, "args");

 string placeholderKey = args.PlaceholderKey;
 MatchCollection matchCollection = Regex.Matches(placeholderKey, @"([/][^|]+[/]*)(?:[|][\d\w]_[\d\w]{8}\-(?:[\d\w]{4}\-){3}[\d\w]{12})*");
 if (matchCollection.Count > 1)
 {
 placeholderKey = String.Empty;
 foreach (Match match in matchCollection)
 {
 if (match.Groups.Count > 1 )
 {
 placeholderKey += match.Groups[1].Value;
 }
 }
 }
 else
 {
 placeholderKey = String.Empty;
 if (matchCollection.Count == 0)
 {
 return;
 }
 foreach (Match match in matchCollection)
 {
 if (match.Groups.Count > 1 && !match.Groups[0].Value.Equals(match.Groups[1].Value))
 {
 placeholderKey += match.Groups[1].Value;
 }
 else
 {
 return;
 }
 }
 }

 // Same as Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings but with fake placeholderKey
 Item placeholderItem = null;
 if (ID.IsNullOrEmpty(args.DeviceId))
 {
 placeholderItem = Client.Page.GetPlaceholderItem(placeholderKey, args.ContentDatabase,
 args.LayoutDefinition);
 }
 else
 {
 using (new DeviceSwitcher(args.DeviceId, args.ContentDatabase))
 {
 placeholderItem = Client.Page.GetPlaceholderItem(placeholderKey, args.ContentDatabase,
 args.LayoutDefinition);
 }
 }

 List<Item> collection = null;
 if (placeholderItem != null)
 {
 bool flag;
 args.HasPlaceholderSettings = true;
 collection = GetRenderings(placeholderItem, out flag);
 if (flag)
 {
 //args.CustomData["allowedControlsSpecified"] = true;
 args.Options.ShowTree = false;
 }
 }
 if (collection != null)
 {
 if (args.PlaceholderRenderings == null)
 {
 args.PlaceholderRenderings = new List<Item>();
 }
 args.PlaceholderRenderings.AddRange(collection);
 }
 }
 }
} 

getChromeData Pipeline

We added our Custom GetDynamicPlaceholderChromeData to the GetChromeData pipeline. This resolves the dynamicPlaceholderKey to retrieve the PlaceholderItem using Sitecore’s code. This pipeline sets the necessary chrome data for the placeholder. All the data you see when you hover over the “Add to Here” comes from this pipeline.  This is handy because if you hover over “Add to Here” you will see the placeholder name.
AddToHere
Code snippets are added below for your reference.

<getChromeData>
    <processor type="Collette.Library.CustomSitecore.Pipelines.GetChromeData.GetDynamicPlaceholderChromeData, Collette.Library" method="Process"/>
</getChromeData>
using System;
using System.Text.RegularExpressions;
using Collette.Library.Util;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetChromeData;
using Sitecore.Web.UI.PageModes;

namespace Collette.Library.CustomSitecore.Pipelines.GetChromeData
{
	public class GetDynamicPlaceholderChromeData : GetChromeDataProcessor
	{
		public override void Process(GetChromeDataArgs args)
		{
			Assert.ArgumentNotNull(args, "args");
			Assert.IsNotNull(args.ChromeData, "Chrome Data");
			if ("placeholder".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
			{
				if (!SitecoreUtil.ShouldRunProcessor(args))
				{
					return;
				}

				var argument = args.CustomData["placeHolderKey"] as string;

				string placeholderKey = argument;
				if (placeholderKey == null)
				{
					return;
				}
				MatchCollection matchCollection = Regex.Matches(placeholderKey, @"([/][^|]+[/]*)(?:[|][\d\w]_[\d\w]{8}\-(?:[\d\w]{4}\-){3}[\d\w]{12})*");
				if (matchCollection.Count > 1)
				{
					placeholderKey = String.Empty;
					foreach (Match match in matchCollection)
					{
						if (match.Groups.Count > 1)
						{
							placeholderKey += match.Groups[1].Value;
						}
					}
				}
				else
				{
					placeholderKey = String.Empty;
					if (matchCollection.Count == 0)
					{
						return;
					}
					foreach (Match match in matchCollection)
					{
						if (match.Groups.Count > 1 && !match.Groups[0].Value.Equals(match.Groups[1].Value))
						{
							placeholderKey += match.Groups[1].Value;
						}
						else
						{
							return;
						}
					}
				}

				// Handles replacing the displayname of the placeholder area to the master reference
				if (args.Item != null)
				{
					string layout = ChromeContext.GetLayout(args.Item);
					Item item = Client.Page.GetPlaceholderItem(placeholderKey, args.Item.Database, layout);
					if (item != null)
					{
						args.ChromeData.DisplayName = item.DisplayName;
					}

					if ((item != null) && !string.IsNullOrEmpty(item.Appearance.ShortDescription))
					{
						args.ChromeData.ExpandedDisplayName = item.Appearance.ShortDescription;
					}
				}
			}
		}
	}
}

Performance Optimization

With atomic design, comes increase in number of renderings on the page.  We noticed that the regex was taking a lot of time.  In order to retrieve the placeholderItem for a given dynamicPlaceholderKey as ” /Page-Content/home-hero/Hero-Carousel-Slide|a_60ebe03e-2f39-4a50-a514-c6387a062d2a/Hero-Slide-Content|b_e781e9f2-38f5-4af1-a550-65156dfe4288″ we only needed the ‘placeholderKey’ value  instead of the entire path for the placeholderKey.

  • So we basically needed “Hero-Slide-Content” instead of “/Page-Content/home-hero/Hero-Carousel-Slide/Hero-Slide-Content”. For this, we replaced the REGEX from both the custom processors  (GetDynamicPlaceholderChromeData and GetDynamicAllowedRenderings). That helped in improving the performance in Page Editor Mode by few seconds.
  • We also have used our custom function to retrieve placeholderItem in GetDynamicPlaceholderChromeData processor. This eliminates the effort of retrieving layout detail of current page for each placeholder item on current page. Thus improving the performance.

So the final code with performance optimization for GetDynamicPlaceholderChromeData processor looks as follows:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Collette.Library.Util;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.Pipelines.GetChromeData;
using Sitecore.SecurityModel;
using Sitecore.Web.UI.PageModes;
using System.Text.RegularExpressions;

namespace Collette.Library.CustomSitecore.Pipelines.GetChromeData
{
	public class GetDynamicPlaceholderChromeData : GetChromeDataProcessor
	{
		public override void Process(GetChromeDataArgs args)
		{
			Assert.ArgumentNotNull(args, "args");
			Assert.IsNotNull(args.ChromeData, "Chrome Data");
			if ("placeholder".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
			{
				var argument = args.CustomData["placeHolderKey"] as string;

				string placeholderKey = argument;
				if (placeholderKey == null)
				{
					return;
				}
				string[] sections = placeholderKey.Split('/');
				if (sections.Length == 0)
				{
					return;
				}

				string lastSection = sections[sections.Length - 1];

				placeholderKey = lastSection.Split('|').FirstOrDefault();
				if (string.IsNullOrEmpty(placeholderKey))
				{
					return;
				}
				// Handles replacing the displayname of the placeholder area to the master reference
				if (args.Item != null)
				{
					Item item = GetPlaceholderItem(placeholderKey, args.Item.Database);
					if (item != null)
					{
						args.ChromeData.DisplayName = item.DisplayName;
					}

					if ((item != null) && !string.IsNullOrEmpty(item.Appearance.ShortDescription))
					{
						args.ChromeData.ExpandedDisplayName = item.Appearance.ShortDescription;
					}
				}
			}
		}

		private Item GetPlaceholderItem(string placeholderKey, Database database)
		{
			Assert.ArgumentNotNull(placeholderKey, "placeholderKey");
			Assert.ArgumentNotNull(database, "database");

			StringBuilder stringBuilder = new StringBuilder("/sitecore/layout/placeholder settings//*[");
			stringBuilder.AppendFormat("comparecaseinsensitive(@{0},'{1}')", (object)"Placeholder Key", (object)placeholderKey);
			int num = placeholderKey.LastIndexOf('/');
			if (num >= 0)
			{
				string str = Sitecore.StringUtil.Mid(placeholderKey, num + 1);
				stringBuilder.AppendFormat(" or comparecaseinsensitive(@{0},'{1}')", (object)"Placeholder Key", (object)str);
			}
			stringBuilder.Append("]");
			Item[] objArray = database.SelectItems(stringBuilder.ToString());
			if (objArray == null)
				return (Item)null;
			return Enumerable.FirstOrDefault<Item>((IEnumerable<Item>)objArray, (Func<Item, bool>)(i =>
			{
				if (i.Fields["Placeholder Key"] != null)
					return i.Fields["Placeholder Key"].Value.Contains("/");
				return false;
			})) ?? Enumerable.FirstOrDefault<Item>((IEnumerable<Item>)objArray);
		}
	}
}

We hope our journey with atomic design helps other teams. Feel free to add comments/share any ideas.

Thanking you.
Collette Dev Team.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s