2

I want to do the following:

  • Name template fields in the format "My Field Name"
  • Avoid filling out Display Name fields for each template field
  • Automatically serialize the field names in camelCase when returning the data from the Layout Service

Where

"fields": {
  "Page Size": {
    "value": 3
  }
}

Becomes

"fields": {
  "pageSize": {
    "value": 3
  }
}

By default, Sitecore does not do this -- it will serialize the field names literally, spaces included. This previous question about template field name conventions in JSS was helpful. The accepted recommendation was to use camelCase for the actual field names in conjunction with display names to make it readable for content authors. This works, but creates what could be considered unnecessary overhead. Further, this approach can be cumbersome when working with legacy codebases.

I have a temporary solution, but I am concerned about the maintainability because I have overridden the Layout Service Field Serializers with a single line of additional code (and had to do so for each field type):

namespace Website.Foundation.LayoutService.Serialization.FieldSerializers
{
  public class NumberFieldSerializer : Sitecore.LayoutService.Serialization.FieldSerializers.NumberFieldSerializer
  {
    public NumberFieldSerializer(IFieldRenderer fieldRenderer) : base(fieldRenderer) { }

      public override void Serialize(Field field, JsonTextWriter writer)
      {
        ...

        writer.WritePropertyName(field.Name.ToJsonPropertyName());
        
        ...
      }
  }
}

I'm looking for a maintainable approach to do this automatically.

To clarify, it's not that I am completely opposed to using Display Name, or rather the Title field of template fields to make them readable to content authors; it's that retrofitting it for hundreds of items becomes rather tedious. Automatic model generation is also a factor. A modern JSS solution written in TypeScript ostensibly calls for automatic model generation both in .cs and .ts files. Variable name format conventions are not the same across languages / frameworks.

The core of this question touches on best practices for C#, JS, Helix, and content authoring in terms of variable / label names.

For example, given a field item named My Field Name, we have these requirements depending on where it's used:

  • JSS: myFieldName
  • C#: MyFieldName (where SomeModel.MyFieldName = {GUID})
  • Content Author: My Field Name

There is no best solution. Each approach will involve code or configuration. The question stands: given a field named My Field Name, what is the most maintainable way to automatically serialize the field name in the format myFieldName in the Layout Service?

5
  • Why do you want to avoid using the Display Name? It is meant for this exact purpose - a nice display name and you can name the item in a programmatic way.
    – Richard Seal
    Jan 10, 2023 at 20:46
  • It's not that I don't want to use Display Name; the issue is more around automating and balancing C#, JS, helix, and content authoring conventions in terms of variable / label names. I added more information to the question. Jan 10, 2023 at 22:25
  • 2
    Are you using the newer edge GraphQL api? or the older layout service calls? In my experience, the Edge graphql end point is already camel casing the field names.
    – Richard Seal
    Jan 11, 2023 at 13:38
  • Currently using the older layout service. Jan 11, 2023 at 14:09
  • It would be overhead on implementation and good to educate the dev team about process so that they can adhere to required coding standard, and proper code review process.
    – Amit Kumar
    Jan 11, 2023 at 17:09

1 Answer 1

2

This is how I did it.

I created a custom content resolver, and it looks like this:

using Newtonsoft.Json.Linq;
using Sitecore.Diagnostics;
using Sitecore.LayoutService.Configuration;
using Sitecore.LayoutService.ItemRendering.ContentsResolvers;
using Sitecore.Mvc.Presentation;
using SitecoreSandbox.Extensions;

namespace SitecoreSandbox.Resolvers
{
    public class GenericResolver : RenderingContentsResolver
    {
        protected Rendering _rendering;
        protected IRenderingConfiguration _renderingConfig;

        public override object ResolveContents(Rendering rendering, IRenderingConfiguration renderingConfig)
        {
            Assert.ArgumentNotNull(rendering, "rendering");
            Assert.ArgumentNotNull(renderingConfig, "renderingConfig");

            _rendering = rendering;
            _renderingConfig = renderingConfig;

            var item = GetContextItem(_rendering, _renderingConfig);

            var jObject = JObject.Parse(_renderingConfig.ItemSerializer.Serialize(item));
            return jObject.ToCamelCase();
        }
    }
}

It uses a custom extension ToCamelCase():

using System.Diagnostics.Contracts;
using System.Linq;
using Newtonsoft.Json.Linq;

namespace SitecoreSandbox.Extensions
{
    public static class JTokenExtensions
    {
        [Pure]
        public static JObject ToCamelCase(this JObject original)
        {
            var newObj = new JObject();
            foreach (var property in original.Properties())
            {
                var newPropertyName = property.Name.Replace(" ", string.Empty).ToCamelCaseString();
                newObj[newPropertyName] = property.Value.ToCamelCaseJToken();
            }

            return newObj;
        }

        // Recursively converts a JToken with PascalCase names to camelCase
        [Pure]
        public static JToken ToCamelCaseJToken(this JToken original)
        {
            switch (original.Type)
            {
                case JTokenType.Object:
                    return ((JObject)original).ToCamelCase();
                case JTokenType.Array:
                    return new JArray(((JArray)original).Select(x => x.ToCamelCaseJToken()));
                default:
                    return original.DeepClone();
            }
        }

        // Convert a string to camelCase
        [Pure]
        public static string ToCamelCaseString(this string str)
        {
            if (!string.IsNullOrEmpty(str))
            {
                return char.ToLowerInvariant(str[0]) + str.Substring(1);
            }

            return str;
        }
    }
}

Then I configured my rendering to use this custom content resolver:

enter image description here

This is what a configuration for a content resolver looks like. Please note that you need to create a similar content resolver for items with child items.

enter image description here

And this is what my ContetBlock template looks like:

enter image description here

And as a bonus, everything is working as expected in the Experience Editor as well:

enter image description here

ContentBlock.tsx looks like this:

import { Text, RichText, Field, withDatasourceCheck } from '@sitecore-jss/sitecore-jss-nextjs';
import { ComponentProps } from 'lib/component-props';

type ContentBlockProps = ComponentProps & {
  fields: {
    heading: Field<string>;
    content: Field<string>;
    cssClass: Field<string>;
  };
};

/**
 * A simple Content Block component, with a heading and rich text block.
 * This is the most basic building block of a content site, and the most basic
 * JSS component that's useful.
 */
const ContentBlock = ({ fields }: ContentBlockProps): JSX.Element => (
  <div className="contentBlock">
    <Text tag="h2" className="contentTitle" field={fields.heading} />

    <RichText className="contentDescription" field={fields.content} />
    <Text tag="p" field={fields.cssClass} />
    <pre>
      <code>{JSON.stringify(fields, null, 2)}</code>
    </pre>
  </div>
);

export default withDatasourceCheck()<ContentBlockProps>(ContentBlock);

Let me know if you have any questions/thoughts or if you find a better approach.

Happy coding! :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.