Set Rendering Parameters in Conditional Rendering

Sitecore has a set “Set Parameters” conditional rendering rule which sets the rendering parameters as the name implies. This rule has some limitations:

  1. It will remove all existing parameters
  2. User will have to know the querystring syntax to add the correct keys and values (e.g. parameter1=value1&parameter2=value2)

The second limitation can be resolved by educating the users to use correct syntax, but there is no option to resolve the first one. There are many legitimate scenarios where you would want to set some rendering parameters and either add some more based on a condition or overwrite the value for a particular parameter.

To support this scenario I created a custom conditional rendering rule to do this:

  1. If there is already a parameter and this is not specified in the conditional rendering rule, then leave it as is.
  2. If there is already a parameter, and the same is also set in the conditional rendering rule, then overwrite it with the value from conditional rendering rule.
  3. If the parameter is only in the conditional rendering rule, add it.

I used below code for my custom action:

using Sitecore.Diagnostics;
using Sitecore.Rules.Actions;
using Sitecore.Rules.ConditionalRenderings;
using System.Linq;

namespace Jeroen.Rules.ConditionalRendering
{
    public class MergeParameterAction<T> : RuleAction<T> where T : ConditionalRenderingsRuleContext
    {
        public string Name { get; set; }

        public string Value { get; set; }

        public override void Apply(T ruleContext)
        {
            Assert.ArgumentNotNull(ruleContext, "ruleContext");

            //grab the existing parameters
            var parameters = Sitecore.Web.WebUtil.ParseUrlParameters(ruleContext.Reference.Settings.Parameters);

            //parameter already there, overwrite
            if (parameters.AllKeys.Contains(Name))
            {
                parameters[Name] = Value;
            }
            //add new parameter
            else
            {
                parameters.Add(Name, Value);
            }

            //add updated parameters back
            ruleContext.Reference.Settings.Parameters =
                string.Join("&", parameters.AllKeys.SelectMany(
                    parameters.GetValues, (n, v) => string.Format("{0}={1}", n, v)));
        }
    }
}

Below screenshot shows how to add the action:

conditional-rendering-custom-action

Now the rule is available in Sitecore and ready to use. You can use the action multiple times in case you need to set multiple parameters as shown here. Now the additional parameters will be set and existing parameters will be kept, there is also no need for the user to remember the syntax, he can just click the name/value pair in the rule editor!

conditional-rendering-using-custom-action

Advertisements

Set any action in conditional rendering on item through personalize screen

The Sitecore UI supports setting any action in global conditional rendering rules, however this is not possible when you try to do this on an item through the personalize button in presentation details. In this case it only supports showing or hiding the component and setting the datasource. First screenshot below shows the editor when you go to the rules in the global conditional rendering (under “/sitecore/system/Marketing Control Panel/Personalization/Rules”). The second screenshot shows the editor when you make a change on the item. Notice that the rules option are limited here.

This is less of an issue if you are using WebForms as you can just make your rule a global rule. With MVC this can become more of a problem as MVC does not support global conditional rendering rules, in MVC you are still able to select a global rule however the rule will not do anything.

Solution

It turns out that this issue is only with the Sitecore UI and any action is supported it is just not possible to select it in the UI. Conditional rendering rules get stored in the Layout of an item in the Renderings and Final Renderings fields. Usually the easiest way to update the layout with the rule is to create the rule first as a Global Rendering rule and then grab the raw value of the rule. Below is an example with a sample rule that logs a message when it’s Sunday. See rule and associated XML below:

log-message-on-sunday

<ruleset>
  <rule uid="{EE27A156-B552-43C0-AFD2-616D9AAE2846}" name="Rule 1">
    <conditions>
      <condition id="{1F15625B-8BDC-4FD2-8F0C-6EE2B8EF0389}" uid="B96A756592C04746856F5F8F9784C3E1" day="{04CC0FD2-C5DE-4F7C-B263-B1C88BABA6CD}" />
    </conditions>
    <actions>
      <action id="{4D151B8B-BD5F-4479-A35F-EE740F6387E8}" uid="5AB9220AD9604A7FABBE48853926B82C" level="Info" text="It's Sunday!" />
    </actions>
  </rule>
</ruleset>

Now copy this XML and update the Renderings or Final Renderings field with it. Here is an example how to update the field with a sample item that has 1 rendering.

Initial:

<r xmlns:p="p" xmlns:s="s" p:p="1">
  <d id="{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}">
    <r uid="{39EE95EC-3BA9-4D8A-AC3E-51DB3FBD353A}">
      
    </r>
  </d>
</r>

With conditional rendering rule added:

<r xmlns:p="p" xmlns:s="s" p:p="1">
  <d id="{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}">
    <r uid="{39EE95EC-3BA9-4D8A-AC3E-51DB3FBD353A}">
      <rls>
        <ruleset>
          <rule uid="{EE27A156-B552-43C0-AFD2-616D9AAE2846}" name="Rule 1">
            <conditions>
              <condition id="{1F15625B-8BDC-4FD2-8F0C-6EE2B8EF0389}" uid="B96A756592C04746856F5F8F9784C3E1" day="{04CC0FD2-C5DE-4F7C-B263-B1C88BABA6CD}" />
            </conditions>
            <actions>
              <action id="{4D151B8B-BD5F-4479-A35F-EE740F6387E8}" uid="5AB9220AD9604A7FABBE48853926B82C" level="Info" text="It's Sunday!" />
            </actions>
          </rule>
        </ruleset>
      </rls>
    </r>
  </d>
</r>

Notice that the difference between the initial XML and the updated one is only an additional “rls” tag with the XML from the Global conditional rendering rule inside it. Now the rule will execute as expected!

Angular 2 and Sitecore Best Practices

Angular 2 is out of beta and I have been using this for a while now to write single page applications in Sitecore. There are a couple of good blog posts around that describes how to get Angular 2 and Sitecore working. This post will focus on some touch points between Sitecore and Angular 2. The official Angular 2 site has a good article which describes setting up Angular 2 in Visual Studio 2015.

Retrieving data from Sitecore

Angular 2 has a HTTP library which makes it easy to work with JSON data returned from the server. Sitecore makes it easy to return JSON data especially from a controller action when using MVC.

However it seems like Sitecore.Services.Client is often overlooked when a restful API is required. Sitecore.Services.Client allows you to fetch Sitecore items in JSON format without writing any code, this link shows several ways to retrieve data this way. The Sitecore.Services.Client is also extensible just like almost everything else in Sitecore. The developer guide can be found here

PushState Routing

Angular 2 uses pushState routing by default, which means that the URL of the Angular route gets appended to the regular URL using a slash (/) as a separator just like Sitecore does. This is the preferred style and is good for SEO as well. An example of this would be an Angular 2 application that is hosted on mysite.com home page that has a route at mysite.com/details which will be displayed by Angular’s routing from the app on home page.

There will be an issue when someone tries to deep link into a mysite.com/details as Sitecore will try to resolve this item but cannot since this is only an Angular route not a Sitecore item. Perhaps the easiest way to solve this is by creating rewrite rules in IIS to resolve these links to the homepage. However I do not like this approach as it is not very maintainable. Creating the same app on a different page would require deploying additional rewrites. Instead I’m using below pipeline to detect this scenario and assign the correct item. Now the content author can add additional instances of the Angular 2 application without involvement from the development team:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.HttpRequest;
using System;

namespace Jeroen.Sitecore
{
    public class Angular2PushStateItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Item == null && Context.Database != null && Context.Database.Name != "core")
            {
                //find all items that have the Angular app
                var query = string.Format("{0}//*[@@templateid='{1}']", args.StartPath, "<Guid of template with Angular 2 app>");
                var items = Context.Database.SelectItems(query);

                foreach (var item in items)
                {
                    var options = LinkManager.GetDefaultUrlOptions();
                    options.LanguageEmbedding = LanguageEmbedding.Never;

                    //check if this is an Angular component inside the current item and if so assign context item
                    if (args.LocalPath.StartsWith(LinkManager.GetItemUrl(item, options), StringComparison.OrdinalIgnoreCase))
                    {
                        Context.Item = item;
                        break;
                    }
                }
            }
        }
    }
}

Here is the code to include the pipeline:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:after="*[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Jeroen.Sitecore.Angular2PushStateItemResolver, Jeroen.Sitecore" />
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

What about SEO, Social and performance?

Angular2 is rendering the page client side which can adversely affect a number of key areas:

  • Page performance as the browser needs to download and run a good amount of javascript to run the application. This can get especially troublesome in mobile and larger more complicated apps
  • SEO as search engine bots are not able to crawl the page
  • Social as Twitter, Facebook and other social media are not able to correctly display a preview

Angular Universal will address these issues by sending the rendered output to the client. This is currently only supported in node.js and ASP .NET core but should hopefully be available for ‘regular’ ASP .NET soon. Therefore I recommend keeping the best practices from the universal team in mind to ensure the application will work in Angular universal when this is ready. Maybe Angular Universal can even get the experience editor working?!