Showing posts with label scriban. Show all posts
Showing posts with label scriban. Show all posts

Wednesday, June 16, 2021

Sitecore Virtual Developer Day 2021

 Sitecore Virtual Developer Day 2021

In March of this Covid year 2021 Sitecore organized their 4th Virtual Developer day. 16 developer-driven sessions from Sitecore MVPs, community developer leaders and Sitecore evangelists. I was honored to be amongst the chosen presenters this year with a session on SXA and Scriban.

The full video of the whole developer day can be found on YouTube. It's one big video for the full 8 hours of content, but you can easily browse the chapters if you want to view a particular session.


Scriban for SXA with unlimited potential

The slides of my session:



The presentation has references to a few blog posts:


And if you are interested in Scriban and SXA, make sure to read the official documentation first.


I hope you enjoyed the Virtual Developer day and my session - if you have any questions, contact the community at StackExchange ðŸ˜‰

Monday, March 9, 2020

Extending Sitecore Scriban to handle User objects

Sitecore SXA & Scriban

As from SXA 9.3 we can use Scriban templates in the variants of SXA. Out of the box, Sitecore provided us with a few extensions that allow us to get some context information and all sorts of stuff on items. We can also fetch item fields and so on...

All of this is very nice and actually rather complete for most cases, but we can still easily write our own extensions.

Access to a User object 

During the 2020 hackathon, we created a Scriban extension to display user information. I tried to return User objects and fetch the properties from within the Scriban template as that would be awesome and very flexible - but it didn't work. Limited in time, I decided to just return the full name of the user instead. But I was curious.. how does this work with Items? So, as any good Sitecore developer, I asked the question on Sitecore StackExchange.
How can I get the user object (or any other object) in the Scriban templates?
Dawid (from the always helpful SXA team) told me it was possible. Not easy (his words), but it could be done - that sounds like a challenge to me 😛

Challenge accepted. And yes, challenge succeeded. Otherwise this would be a silly blog post...

Object Accessor

First step in the process is an object accessor - a UserAccessor in our case - that has the logic to access the properties of the object from within Scriban. It's a class that implements Scriban.Runtime.IObjectAccessor.
using System;
using System.Collections.Generic;
using System.Linq;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Sitecore.Security;
using Sitecore.Security.Accounts;

public class UserAccessor : IObjectAccessor
{
  public int GetMemberCount(TemplateContext context, SourceSpan span, object target)
  {
    return GetMembers(context, span, target).Count();
  }

  public IEnumerable<string> GetMembers(TemplateContext context, SourceSpan span, object target)
  {
    var user = target as User;
    if (user == null)
    {
      return Enumerable.Empty<string>();
    }

    var properties = typeof(UserProfile).GetProperties().Where(p => p.GetType().IsValueType);
    var result = properties.Select(p => p.Name);
    return result;
  }

  public bool HasMember(TemplateContext context, SourceSpan span, object target, string member)
  {
    return true;
  }

  public bool TryGetValue(TemplateContext context, SourceSpan span, object target, string member, out object value)
  {
    value = string.Empty;
    var user = target as User;
    if (user == null)
    {
      return false;
    }

    try
    {
      var property = typeof(UserProfile).GetProperty(member);
      if (property == null)
      {
        return false;
      }

      value = property.GetValue(user.Profile, null);
      return true;

    }
    catch
    {
      return false;
    }
  }

  public static string ToString(User user)
  {
    return user.Profile.FullName;
  }

  public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object value)
  {
    throw new InvalidOperationException("Unable to change user properties during the rendering process");
  }
}

What we have here:

  • GetMemberCount: return the number of members (see next function)
  • GetMembers: I want to be able to use all value typed properties (string and such) of a users profile. After checking if the object actually is a user, we fetch all those properties from the "UserProfile" type and return their names.
  • HasMember: as we have members, this is just true
  • TryGetValue: this function tries to get the value for the given member from the object. We first verify the object is a User. Then we try to get the property named "member" from the UserProfile type. If the property exists (note this is case sensitive) we can get the value of that property from the given object (User) - in our case it's the user.Profile and not the user itself. The return value indicates whether the value was found or not - we return an empty string on all failures.
  • ToString: function to determine what happens if the object is used without a member in the Scriban template - see later in the Context
  • TrySetValue: we don't allow set functionality

Template Context

In the template context all accessors are registered. We inherit from the current Sitecore.XA.Foundation.Scriban.ContextExtensions.SitecoreTemplateContext and override a few functions:
using Scriban.Parsing;
using Scriban.Runtime;
using Sitecore.Security.Accounts;
using Sitecore.XA.Foundation.Mvc.Models;
using Sitecore.XA.Foundation.Scriban.ContextExtensions;

public class GatoTemplateContext : SitecoreTemplateContext
{
  protected UserAccessor UserAccessor { get; }

  public GatoTemplateContext(RenderingWebEditingParams webEditingParams) : base(webEditingParams)
  {
    UserAccessor = new UserAccessor();
  }

  protected override IObjectAccessor GetMemberAccessorImpl(object target)
  {
    return target is User ? UserAccessor : base.GetMemberAccessorImpl(target);
  }

  public override string ToString(SourceSpan span, object value)
  {
    return value is User user ? UserAccessor.ToString(user) : base.ToString(span, value);
  }
}

The three functions that we override:

  • Constructor: we call the base constructor and initialize the UserAccessor property
  • GetMemberAccessorImpl: this method decides which accessor to use. We check if the target object is a User - if so we return the UserAccessor, if not we call the base method.
  • ToString: the general ToString method - we need to check the target object and if that is a User we call the ToString we created in the UserAccessor, otherwise we call the base method.

InitializeScribanContext

Next and last step is a processor to set our template context. The first call in the generateScribanContext pipeline is the current Scriban context initializer. We create a new version of this processor - implementing Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext.IGenerateScribanContextProcessor.
using Scriban.Runtime;
using Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext;

public class InitializeScribanContext : IGenerateScribanContextProcessor
{
  public void Process(GenerateScribanContextPipelineArgs args)
  {
    args.GlobalScriptObject = new ScriptObject();
    args.TemplateContext = new GatoTemplateContext(args.RenderingWebEditingParams);
  }
}

This processor sets our template context - we have to configure this processor instead of the existing one:
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:search="http://www.sitecore.net/xmlconfig/search/">
  <sitecore>
    <pipelines>
      <generateScribanContext>
        <processor type="Feature.Attendees.Subscribers.InitializeScribanContext, Feature.Attendees" patch:instead="*[@type='Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext.InitializeScribanContext, Sitecore.XA.Foundation.Scriban']" resolve="true" />
      </generateScribanContext>
    </pipelines>
  </sitecore>
</configuration>

And that's is for code. This example used the Sitecore User object, but we could enable other objects the same way.


Scriban template

To use this in a Scriban template, you need a function that returns one or more User objects. Such a function is out-of-scope for this post, but you can easily create that with a custom Scriban extension.
<div>
<ul>
{{for i_user in (sc_subscribers i_page)}}
  <li>{{i_user}} - {{i_user.Email}} - {{i_user.Portrait}}</li>
{{end}}
</ul>
</div>

The sc_subscribers function in the example is a custom function that returns users. As you can see we can then write {{i_user}} (this will use the ToString method) and also things like {{i_user.Email}} to get the profile properties of a user.

Pretty cool stuff.. making the Scriban templates even more useful!


from Cat GIFs via Gfycat


Wednesday, November 6, 2019

Custom SXA Scriban extensions

Sitecore - SXA & Scriban


As explaining in the what's new post on SXA 9.3 one of the major new features in the Sitecore Experience Accelerator version 9.3 is the rendering variant with a Scriban template.

Sitecore's SXA team provided a whole set of extensions that will help you get almost anything done. And if that is not the case, you can easily write your own extension and that is what I will show you here.

We'll create a custom function that can optionally utilize the Sitecore context and an extension function to call on items in Scriban.

Custom Scriban context function

A context function can perform any logic based on the parameters it receives using the Sitecore context if needed. Let's see a simple example:

Code

using Scriban.Runtime;
using Sitecore.XA.Foundation.Abstractions;
using Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext;

public class ScribanMeow : IGenerateScribanContextProcessor
{
    private readonly IContext context;

    public ScribanMeow(IContext context)
    {
        this.context = context;
    }

    public void Process(GenerateScribanContextPipelineArgs args)
    {
        var meow = new MeowIt(Meow);
        args.GlobalScriptObject.Import("sc_meow", (Delegate)meow);
    }

    public string Meow(string key)
    {
        return key + " Miauw";
    }

    private delegate string MeowIt(string key);
}
We create a class implementing the IGenerateScribanContextProcessor. We inject the IContext in our constructor. In this example we're not using it, but this shows that you can if you need to. The Process function will attach our delegate to the Scriban function that we can name as we want (in this example the name is "sc_meow").

The delegate can have parameters as needed - here just a string but have more and/or other object types. The implementation of the delegate contains the logic you want performed by the function (in this example case we are just meowing the given text).

Config

<sitecore>
  <pipelines>
    <generateScribanContext>
      <processor type="Sxa93.ScribanMeow, Sxa93" resolve="true" />
    </generateScribanContext>
  </pipelines>
</sitecore>
We are placing our processor in the generateScribanContext pipeline - don't forget to add the resolve element if you want the context to get resolved in your constructor.

Usage

Once this is all done, we can use our function by calling it in a Scriban template. In our example that would be like {{ sc_meow "Key" }}.



Custom Scriban item member

An item member for Scriban is like an extension method on the Item class in C#. It's a function that gets an item and returns a value based on some logic.

Code

using Sitecore.XA.Foundation.Abstractions;
using Sitecore.XA.Foundation.Scriban.Pipelines.GetScribanItemMembers;

public class GetUpdated : GetScribanItemMember
{
    private readonly IContext context;

    protected override string MemberName => "updated";

    public GetUpdated(IContext context)
    {
        this.context = context;
    }

    protected override void Resolve(GetScribanItemMembersPipelineArgs args)
    {
        if (args.Mode == MemberMode.Name)
            args.Members.Add(MemberName);
        else
            args.MemberValue = (object) args.Item.Statistics.Updated;
    }
}
We create a class inheriting from GetScribanItemMember. We inject the IContext in our constructor. In this example we're not using it, but this shows that you can if you need to. We override the MemberName to the name we want for our extension (in this example "updated") and implement the abstract Resolve function. In this function we can place our logic - anything we want to get some output (in this example we return the updated date from the item statistics).

Config

<sitecore>
  <pipelines>
    <getScribanItemMembers>
      <processor type="Sxa93.GetUpdated, Sxa93" resolve="true" />
    </getScribanItemMembers>
  </pipelines>
</sitecore>
We are placing our class in the getScribanItemMembers pipeline - don't forget to add the resolve element if you want the context to get resolved in your constructor.

Usage

Once this is all done, we can use our item member by calling it in a Scriban template on an item. In our example that would be like {{ i_item.updated }} or {{ i_page.updated }} (or any other item object).


Conclusion

Sitecore has provided many useful extension inside SXA already, but if you do need to extend it you can. It's actually not that much work and very flexible.  Enjoy!


What's new in Sitecore SXA 9.3 - Scriban

SXA 9.3

ScribanSXA 9.3 is the successor of 1.9 - they took over the version number of Sitecore. As usual, a few new features are released - like the Aggregated Facet Filter and Facets Summary components.  One of the most eye-catching newbies however is the introduction of Scriban.

Scriban

Scriban is a fast, powerful, safe and lightweight text templating language and engine. It is introduced in the rendering variant system, so now you can add a Scriban template there.  The SXA team released a bunch of extension to Scriban to help us. We can now use Sitecore functions and objects inside a Scriban template.

Scriban SXA
All those extensions are documented by Sitecore itself, so I'm not going to repeat that information. But let's have a look at what we can do with this templating engine based upon a few issues I had before that seems to have an easier solution now. Just to have a few examples...

Examples

Data attributes

As described on https://ggullentops.blogspot.com/2018/10/using-tokens-in-sitecore-sxa-variant.html I had an issue to add classes to variants based upon a droplist field.
By using the Scriban variant it seems we can solve this a lot easier:
<div class="{{ sc_follow i_item "Color" | sc_raw "Title" }}"> ... </div>
What are we doing here? We use:
  1. sc_follow to get a target item in the Color field of i_item
  2. i_item to get the current context item (and pass it to sc_follow)
  3. | to pass this item to the next function
  4. sc_raw because to get the raw value of the Title field
Note that in "normal" situations you could use sc_field to get the value of a field - making it editable in the experience editor. But exactly that benefit makes it an issue here and an editable field inside a class parameter actually broke my experience editor. But it's good to have all options open.

And - we can achieve the same outcome with the short notation:
{{ i_item.Color.Title.raw }}

External data

On https://ggullentops.blogspot.com/2019/03/external-model-data-SXA-variant-Sitecore.html I wrote about showing external data with SXA. I mentioned the Model variant item which still exists and we have a Model Iterator variant now as well (since 1.9). This variant will iterate over your external data (with max and skip parameters). With the introduction of the processSearchItems pipeline in 1.9 (adding an object to your search results) this might become even more important.
Focusing on Scriban here, there is also a way to access that information in the Scriban templates using the o_model context object.

Translations - Dictionary

As we were facing text values in our variants (using the "Text" variant) and we did not want our editors to have to edit/translate those texts there, we wrote a custom variant definition to solve this issue. That solution is still valid, but there is an alternative now.  When you create a new site with SXA in 9.3 you will see a new item in your site root:
SXA Dictionary
The (display) name of the item should be "Dictionary - SITENAME".  This is the root item for the new per site Dictionary that was introduced. You can create dictionary entries (and folders) here, just as you can underneath the /system/Dictionary that is already known. That system dictionary is a fallback in case the key is not found in the site dictionary. You can also define a first level fallback dictionary from another site in the instance (to be able to share dictionary entries across multiple sites).

To get a value from this Dictionary inside your variant, you can call the sc_translate function in a Scriban template (pass the key as parameter).

sc_execute

SXA provided a lot of extensions that are very useful and well described - but I want to dive just a little deeper into one: sc_execute. The reason is that this one gives immense opportunities. The function executes a rendering variant field located in the Sitecore tree beneath the Scriban template.

When I got introduced to Scriban & SXA 9.3 I got the example of a responsive image. There is not (yet) a way to show those inside the template, so you could use sc_execute to get them inside the template. That nice.. but we could take it a step further. We also have the Component variant - since v1.8.

When we combine both we can create Scriban templates that contain other variants that actually render other components. Maybe even add a little rules engine sauce to it, and we have a tremendous cocktail that will blow your mind. 

Just to show we can really do a lot with this. A challenge might be to find a good balance with the templates to keep it all maintainable. 

Custom

We can also create our own Scriban extensions - that will be the next blog post.



As a side note.. if you have used NVelocity in your current solution :
Breaking change
NVelocity was removed from the Sitecore system.
Variants based on NVelocity will no longer work.