Wednesday, April 10, 2019

Sitecore SXA custom rendering variant field for translations

The requirement

We had to display a field (from the datasource item) and some fixed labels in a html structure.

So for a first version we just created the rendering variant and added Field and Text variants to the definition. This works..  but in our multilingual environment we have to translate the labels. This can be done as the values are not shared, but I started thinking.. 
do I want content editors that translate labels changing my rendering variants?
And the answer was no..

To fix that, I created my own variant definition to support translated labels.

 Creating a custom variant field

Template

First thing to do is create a template to define the fields of the variant. In our case that was very easy as I just wanted to create an enhanced version of "Text", so I used that template (/sitecore/templates/Foundation/Experience Accelerator/Rendering Variants/VariantText) as base template and added nothing else.

If you have another use case to create a custom variant field, check the existing ones to see what base templates you need. A good start will be: 
  • /sitecore/templates/Foundation/Experience Accelerator/Rendering Variants/Base/_Rendering Variants Base
  • /sitecore/templates/Foundation/Experience Accelerator/Rendering Variants/Base/_Data Attributes
  • /sitecore/templates/Foundation/Experience Accelerator/Rendering Variants/Base/_Link Attributes
  • /sitecore/templates/Foundation/Experience Accelerator/Variants/Base Variant Field
Remember not to place your template in the SXA folders! Use your own folder structure...

Insert Options

You want your field to be available in the insert options when people create a variant definition. The best practices tell us not to change the SXA templates, so we won't do that. We'll use the rules engine to add the insert options!

Go to /sitecore/system/Settings/Rules/Insert Options/Rules and add your insert rule:
You need to check the item template to detect the templates you want to add the insert option to (the variant definition itself and all rendering variant templates in my case), and add your template as insert option.

Corey Smith wrote an excellent blog post on organizing your insert options - a good idea, you should read this ;)

The code

First class we need is a model for your template. It should inherit from Sitecore.XA.Foundation.RenderingVariants.Fields.RenderingVariantFieldBase, but as we are building upon VariantText I inherited from that one (in the same namespace).
public class DictionaryText : VariantText
{
  public DictionaryText(Item variantItem) : base(variantItem)
  {
  }

  public static new string DisplayName => "Dictionary Text";
}

Creating instances of this DictionaryText class is done with a processor class that inherits from Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.ParseVariantFields.ParseVariantFieldProcessor.  In that class you need to define the supported template (the guid of the template you created earlier) and the TranslateField function that translates the variant field arguments to the model class. I checked the VariantText implementation (as we have the same model):
public class ParseDictionaryText : ParseVariantFieldProcessor
{
  public override ID SupportedTemplateId => new ID("{AAD9B54E-C7B2-4193-975E-954C0AD5A922}");

  public override void TranslateField(ParseVariantFieldArgs args)
  {
    var variantFieldArgs = args;
    var variantText = new DictionaryText(args.VariantItem);
    variantText.ItemName = args.VariantItem.Name;
    variantText.Text = args.VariantItem[Sitecore.XA.Foundation.RenderingVariants.Templates.VariantText.Fields.Text];
    variantText.Tag = args.VariantItem.Fields[Sitecore.XA.Foundation.RenderingVariants.Templates.VariantText.Fields.Tag].GetEnumValue();
    variantText.IsLink = args.VariantItem[Sitecore.XA.Foundation.RenderingVariants.Templates.VariantText.Fields.IsLink] == "1";
    variantText.LinkField = args.VariantRootItem[Sitecore.XA.Foundation.Variants.Abstractions.Templates.IVariantDefinition.Fields.LinkField];
    variantText.CssClass = args.VariantItem[Sitecore.XA.Foundation.RenderingVariants.Templates.VariantText.Fields.CssClass];
    variantFieldArgs.TranslatedField = variantText;
  }
}
Note that the ID in this example is the ID of my template - you will probably have another one ;)

Next step is to create the processor that actually renders the variant. Create a class that inherits from Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.RenderVariantField.RenderRenderingVariantFieldProcessor. You need to set a few properties to define the supported template and the render mode (html/json) but the main part is the RenderField function.
In this RenderField you can add any logic you want and render the output. In our case, we stay very close to the code from the ootb VariantText but just translate the value inside:
public class RenderDictionaryText : RenderRenderingVariantFieldProcessor
{
  public override Type SupportedType => typeof(DictionaryText);

  public override RendererMode RendererMode => RendererMode.Html;

  public override void RenderField(RenderVariantFieldArgs args)
  {
    if (!(args.VariantField is DictionaryText variantField))
    {
      return;
    }

    var dictionaryRepository = ServiceLocator.ServiceProvider.GetService<IDictionaryRepository>();
    var control = (Control)new LiteralControl(dictionaryRepository.GetValue(variantField.Text));
    if (variantField.IsLink)
    {
      control = InsertHyperLink(control, args.Item, variantField.LinkAttributes, variantField.LinkField, false, args.HrefOverrideFunc);
    }

    if (!string.IsNullOrWhiteSpace(variantField.Tag))
    {
      var tag = new HtmlGenericControl(variantField.Tag);
      AddClass(tag, variantField.CssClass);
      AddWrapperDataAttributes(variantField, args, tag);
      MoveControl(control, tag);
      control = tag;
    }

    args.ResultControl = control;
    args.Result = RenderControl(args.ResultControl);
  }
}
Note that I am using a custom DictionaryRepository here.. you'll need to change those two lines of code with your own code to translate the text.

Configuration
Last step is adding the processors to the correct pipelines in a config patch - e.g.:
<sitecore>
  <pipelines>
    <parseVariantFields>
      <processor type="Foundation.Dictionary.RenderingVariants.ParseDictionaryText, Foundation.Dictionary" resolve="true"/>
    </parseVariantFields>
    <renderVariantField>
      <processor type="Foundation.Dictionary.RenderingVariants.RenderDictionaryText, Foundation.Dictionary" resolve="true"/>
    </renderVariantField>
  </pipelines>
</sitecore>


Conclusion

My editors are happy. They can change and translate all labels as they are used to and don't need to think about the variants.

My developers are happy because the editors don't mess around in the variants just to translate a label.

And I did not have to spend so much time doing this...  but it seemed worth to share the idea (and the code).

2 comments:

  1. Hi Gert - thanks for the awesome blog post! I'm trying this myself and ran into a problem. Wonder if you have any insight.

    In my render processor, I'm trying to create an image control. The code assigns the src as well as an alt attribute and some data-attributes (specifically, loading=lazy), but when the result is streamed to the browser, the src attribute is missing. I've stepped through the code and the src is there when assigning the result of RenderControl, but on the page it's empty.

    Code:
    var image = new System.Web.UI.HtmlControls.HtmlImage();
    image.Src = url;
    image.Alt = "todo";

    var attributes = variant.DataAttributes.AllKeys.SelectMany(variant.DataAttributes.GetValues, (k, v) => new { key = k, value = v });
    foreach (var attribute in attributes)
    {
    image.Attributes.Add(attribute.key, attribute.value);
    }

    args.ResultControl = image;
    args.Result = RenderControl(args.ResultControl);


    Result: < img alt="todo" loading="lazy" >

    Any ideas?

    Cheers,

    Jordan

    ReplyDelete
    Replies
    1. Hi Jordan,

      You should ask questions like this on https://sitecore.stackexchange.com/ - this way the whole Sitecore community can help you. And that platform is better suited for Q&A..

      Delete