Module: Page Builder

6 of 18 Pages

Style the template

You probably noticed in the last section that the template is not very pretty yet. You’ll also find that the dropdowns for color scheme and corner style in the template properties don’t change anything.

Let’s create a service that maps the options from the dropdown to CSS classes, and a tag helper that uses it to style a div element.

Create a service to retrieve styles

  1. Define an IComponentStyleEnumService interface in the ~/Features/Shared/Services folder.
  2. Include method signatures to retrieve CSS classes based on a ColorSchemeOption and CornerStyleOption respectively.
    C#
    IComponentStyleEnumService.cs
    
     using TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme;
     using TrainingGuides.Web.Features.Shared.OptionProviders.CornerStyle;
    
     namespace TrainingGuides.Web.Features.Shared.Services;
    
     public interface IComponentStyleEnumService
     {
         IEnumerable<string> GetColorSchemeClasses(ColorSchemeOption colorScheme);
    
         IEnumerable<string> GetCornerStyleClasses(CornerStyleOption cornerStyle);
         ...
     
  3. Add method signatures to convert the string representation of a selected option to its corresponding enum value, and to retrieve the style for links.
    C#
    IComponentStyleEnumService.cs
    
     ...
     CornerStyleOption GetCornerStyle(string cornerStyleString);
    
     ColorSchemeOption GetColorScheme(string colorSchemeString);
    
     ColorSchemeOption GetLinkStyle(string linkStyleString);
     }
     
  4. Implement the interface, mapping the selected options to sets of CSS classes. Use the Parse method from the EnumStringService we defined earlier in the series for the string conversion methods.
    C#
    ComponentStyleEnumService.cs
    
     using TrainingGuides.Web.Features.Shared.OptionProviders;
     using TrainingGuides.Web.Features.Shared.OptionProviders.ColorScheme;
     using TrainingGuides.Web.Features.Shared.OptionProviders.CornerStyle;
    
     namespace TrainingGuides.Web.Features.Shared.Services;
    
     public class ComponentStyleEnumService(IEnumStringService enumStringService) : IComponentStyleEnumService
     {
         public IEnumerable<string> GetColorSchemeClasses(ColorSchemeOption colorScheme) => colorScheme switch
         {
             ColorSchemeOption.Light1 => ["tg-bg-light-1", "tg-txt-dark"],
             ColorSchemeOption.Light2 => ["tg-bg-light-2", "tg-txt-dark"],
             ColorSchemeOption.Light3 => ["tg-bg-light-3", "tg-txt-dark"],
             ColorSchemeOption.Dark1 => ["tg-bg-primary", "tg-txt-light"],
             ColorSchemeOption.Dark2 => ["tg-bg-secondary", "tg-txt-light"],
             ColorSchemeOption.TransparentLight => ["tg-bg-none", "tg-txt-light"],
             ColorSchemeOption.TransparentMedium => ["tg-bg-none", "tg-txt-medium"],
             ColorSchemeOption.TransparentDark => ["tg-bg-none", "tg-txt-dark"],
             _ => [string.Empty],
         };
    
         public IEnumerable<string> GetCornerStyleClasses(CornerStyleOption cornerStyle) => cornerStyle switch
         {
             CornerStyleOption.Round => ["tg-corner-rnd"],
             CornerStyleOption.VeryRound => ["tg-corner-v-rnd"],
             CornerStyleOption.Sharp => ["tg-corner-shrp"],
             _ => [string.Empty],
         };
    
         public CornerStyleOption GetCornerStyle(string cornerStyleString) =>
             enumStringService.Parse(cornerStyleString, CornerStyleOption.Round);
    
         public ColorSchemeOption GetColorScheme(string colorSchemeString) =>
             enumStringService.Parse(colorSchemeString, ColorSchemeOption.TransparentDark);
    
         public ColorSchemeOption GetLinkStyle(string linkStyleString)
         {
             var colorScheme = enumStringService.Parse(linkStyleString, LinkStyleOption.TransparentDark);
             return (ColorSchemeOption)colorScheme;
         }
     }
     

    Each of the classes in this example are small, usually only setting one css property to minimize collisions across classes.

    Defining the styles themselves is outside the scope of this series, but you can find their definitions in the ~/scss directory, where .scss styles are defined and later compiled to css.

    If you are using the main branch of the Training guides repository to follow along, the classes should already exist.
  5. Remember to register the service with the dependency injection container in the TrainingGuides.Web/ServiceCollectionExtensions.cs file:
    C#
    ServiceCollectionExtensions.cs
    
     ...
     public static void AddTrainingGuidesServices(this IServiceCollection services)
     {
         ...
         services.AddSingleton<IComponentStyleEnumService, ComponentStyleEnumService>();
         ...
     }
     ...
     

Apply the styles with a tag helper

  1. Add a new class called ComponentStyleTagHelperto the ~/Features/Shared/Helpers/TagHelpers folder.
  2. Inherit from the .NET TagHelper class, and decorate it with the HtmlTargetElement attribute to set its element name to tg-component-style. Acquire an IComponentStyleEnumService object through dependency injection.
    C#
    ComponentStyleTagHelper.cs
    
     ...
     [HtmlTargetElement("tg-component-style")]
     public class ComponentStyleTagHelper(
         IComponentStyleEnumService componentStyleEnumService) : TagHelper
     ...
     
  3. Give the tag helper string properties called ColorScheme and CornerStyle, so that objects that store selections as strings, such as page template properties, can use them directly.
    C#
    ComponentStyleTagHelper.cs
    
         ...
         public string ColorScheme { get; set; } = string.Empty;
         public string CornerStyle { get; set; } = string.Empty;
         ...
     
  4. Use a constant to hold the type of tag, in case we need to add more complex logic for determining the tag name in the future.
    C#
    ComponentStyleTagHelper.cs
    
     ...
     private const string DIV_TAG = "div";
     ...
     
  5. In the Process method, use the service to retrieve styles for the output tag.
    C#
    ComponentStyleTagHelper.cs
    
     ...
     public override void Process(TagHelperContext context, TagHelperOutput output)
     {
         output.TagName = DIV_TAG;
    
         List<string> cssClasses = [];
    
         var colorScheme = componentStyleEnumService.GetColorScheme(ColorScheme);
         cssClasses.AddRange(componentStyleEnumService.GetColorSchemeClasses(colorScheme));
    
         var cornerStyle = componentStyleEnumService.GetCornerStyle(CornerStyle);
         cssClasses.AddRange(componentStyleEnumService.GetCornerStyleClasses(cornerStyle));
    
         if (cssClasses.Count > 0)
         {
             foreach (string cssClass in cssClasses)
             {
                 output.AddClass(cssClass, HtmlEncoder.Default);
             }
         }
     }
     ...
     
In the end, your tag helper should look like this:
C#
ComponentStyleTagHelper.cs

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using TrainingGuides.Web.Features.Shared.Services;

namespace TrainingGuides.Web.Features.Shared.Helpers.TagHelpers;

[HtmlTargetElement("tg-component-style")]
public class ComponentStyleTagHelper(
    IComponentStyleEnumService componentStyleEnumService) : TagHelper
{
    public string ColorScheme { get; set; } = string.Empty;
    public string CornerStyle { get; set; } = string.Empty;

    private const string DIV_TAG = "div";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = DIV_TAG;

        List<string> cssClasses = [];

        var colorScheme = componentStyleEnumService.GetColorScheme(ColorScheme);
        cssClasses.AddRange(componentStyleEnumService.GetColorSchemeClasses(colorScheme));

        var cornerStyle = componentStyleEnumService.GetCornerStyle(CornerStyle);
        cssClasses.AddRange(componentStyleEnumService.GetCornerStyleClasses(cornerStyle));

        if (cssClasses.Count > 0)
        {
            foreach (string cssClass in cssClasses)
            {
                output.AddClass(cssClass, HtmlEncoder.Default);
            }
        }
    }
}

Put the tag helper to use

The tag helper is now ready to be added to the view. If you return to ServicePagePageTemplate.cshtml you can wrap all of the existing markup with the new tag helper tag, and pass along the color scheme and corner style properties from the page template’s configuration.
cshtml
ServicePagePageTemplate.cshtml

    @using TrainingGuides.Web.Features.FinancialServices.Models
    @using TrainingGuides.Web.Features.FinancialServices

    @model TemplateViewModel<ServicePagePageTemplateProperties>

    @{
        var templateModel = Model.GetTemplateModel<ServicePageViewModel>();
    }

    <tg-component-style color-scheme="@Model.Properties.ColorScheme" corner-style="@Model.Properties.CornerStyle">
        <div>
            <div>
                <h3>@templateModel.NameHtml</h3>
                <p>@templateModel.ShortDescriptionHtml</p>
            </div>
            
            <div>
                <img src="@templateModel.Media.FirstOrDefault()?.FilePath" alt="@templateModel.Media.FirstOrDefault()?.Description"/>
            </div>
        </div>
    </tg-component-style>

If you sign in to the Xperience administration interface, you’ll notice that you can now adjust the appearance of the box containing the service’s data by clicking the gear icon in the bottom left of the Page Builder pane.

However, if you set the template to use rounded corners and a colored background, you’ll see that the corners of the image do not change to match the style of the parent div. The image may also be an unreasonable size, depending on the file.

Screenshot of incomplete template with rounded corners set. Also showing the Configure template button.

Create an image tag helper

  1. In the ~/Features/Shared/Helpers/TagHelpers folder, add a new self-closing TagHelper called StyledImageTagHelper with the tag name tg-styled-image. Inject an IComponentStyleEnumService instance.
    C#
    StyledImageTagHelper.cs
    
     ...
     namespace TrainingGuides.Web.Features.Shared.Helpers.TagHelpers;
    
     [HtmlTargetElement("tg-styled-image", TagStructure = TagStructure.WithoutEndTag)]
     public class StyledImageTagHelper(
         IComponentStyleEnumService componentStyleEnumService) : TagHelper
     ...
     
  2. Set up properties in the same manner as the ComponentStyleTagHelper, leaving out the ColorScheme property this time.
    C#
    StyledImageTagHelper.cs
    
     ...
    
     public string CornerStyle { get; set; } = string.Empty;
    
     private const string IMG_TAG = "img";
     ...
     
  3. In the Process method, set the TagMode of the rendered HTML tag to TagMode.SelfClosing and style the image according to the classes returned by the service.
    C#
    StyledImageTagHelper.cs
    
     ...
     public override void Process(TagHelperContext context, TagHelperOutput output)
     {
         output.TagName = IMG_TAG;
         output.TagMode = TagMode.SelfClosing;
    
         List<string> cssClasses = [];
    
         var cornerStyle = componentStyleEnumService
             .GetCornerStyle(CornerStyle ?? string.Empty);
         cssClasses.AddRange(componentStyleEnumService
             .GetCornerStyleClasses(cornerStyle));
    
         if (cssClasses.Count > 0)
         {
             foreach (string cssClass in cssClasses)
             {
                 output.AddClass(cssClass, HtmlEncoder.Default);
             }
         }
     }
     ...
     
The resulting tag helper should look like this:
C#
StyledImageTagHelper.cs

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using TrainingGuides.Web.Features.Shared.Services;

namespace TrainingGuides.Web.Features.Shared.Helpers.TagHelpers;

[HtmlTargetElement("tg-styled-image", TagStructure = TagStructure.WithoutEndTag)]
public class StyledImageTagHelper(
    IComponentStyleEnumService componentStyleEnumService) : TagHelper
{
    public string CornerStyle { get; set; } = string.Empty;

    private const string IMG_TAG = "img";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = IMG_TAG;
        output.TagMode = TagMode.SelfClosing;

        List<string> cssClasses = [];

        var cornerStyle = componentStyleEnumService.GetCornerStyle(CornerStyle ?? string.Empty);
        cssClasses.AddRange(componentStyleEnumService.GetCornerStyleClasses(cornerStyle));

        if (cssClasses.Count > 0)
        {
            foreach (string cssClass in cssClasses)
            {
                output.AddClass(cssClass, HtmlEncoder.Default);
            }
        }
    }
}

The tag helper can be used in the view like this:

cshtml
Tag helper usage (e.g., in ServicePagePageTemplate.cshtml)

@foreach (var asset in templateModel.Media)
{
    <tg-styled-image src="@asset.FilePath" 
        alt="@asset.Description" 
        corner-style="@Model.Properties.CornerStyle" 
        class="c-product-img object-fit-cover" />
}