Build a simple call-to-action widget
Widgets are Page Builder components of the smallest granularity. While sections, discussed in the previous guide, serve primarily to change the page layout and appearance, widgets are a tool for your editors to display and style structured data or take input from the live site visitor.
In this guide, we will create a simple widget based on a view component. Let’s call it Simple call to action (CTA) widget.
Editors can use the widget anywhere they need a stand-alone CTA button. For example, the “Contact us” or the “View downloads” button from the mockups we talked about in the first guide of this Page Builder series:
Prerequisites
This guide assumes you have:
- basic knowledge of C# and .NET framework concepts
- familiarity with the basics of Page Builder in Xperience by Kentico
To follow along with the example in this guide, the main branch of our training guides repository is a great starting point. If you have already been using the repository to follow along with earlier guides in this series, you are at the perfect spot.
Our .NET solution contains two projects. In this guide we will be working solely in the TrainingGuides.Web, so you can assume that being the root at all times.
If you are working on your own project, make sure your instance of Xperience by Kentico is version 29.6.1 or higher.
The code samples in this guide are for .NET 8 only.
To see finished implementation of Simple CTA widget, check out this folder in the finished branch of our repository.
If you browse the repository, you may notice another Call to action widget, similar to this Simple CTA. It is different example of the CTA button, as seen in the KBank demo site, unrelated to this guide.
Understand the requirements
Editor working with the Simple CTA widget should be able to:
- set the call to action text (e.g., Contact us)
- configure whether the button click navigates to a page or an external link
- select an existing page, or paste in an external link to navigate to
- choose whether or not the target opens in a new browser tab
Implement the widget
From an editor’s perspective, the widget has two parts:
- UI component - the button that renders on the page.
- configurable properties - a form the editor can access by clicking on the gear icon of the widget
From a developer’s perspective, the widget will consist of four files:
- properties - defines the configurable properties form, for the editor to interact with
- view component - defines how the property values affect the widget behavior and how they map to the widget view model
- view model - the data model for the widget view to render
- view - razor view of the widget. It renders in the result webpage for visitors. Editors can also see it in the Page Builder view and page preview in the Xperience administration.
Start by preparing your folder structure:
- Create a new Widgets folder inside ~/Features/LandingPages. Here you can place any future widgets related to the Landing pages feature.
- Inside the Widgets folder, create a SimpleCallToAction folder to hold your new widget’s files.
Define widget properties
Based on our specification, the Simple CTA button needs five configurable properties for the editor:
- Call to action text - text field to enter the button text
- Target content - selector for the editor to chose type of target (page or an external link)
- Target page - page selector, shown only if the editor choses to target page
- Absolute URL - text field to paste in target URL, shown only if the editor targets external link
- Open in new tab - checkbox to open the target in the new tab
To define the configurable properties, we will use the set of existing Xperience admin UI form components.
- First, create a SimpleCallToActionWidgetProperties.cs file in your SimpleCallToAction folder, to hold your property definitions.
Based on this file, Xperience will know how to render the UI controls in the administration interface.
- Define a
SimpleCallToActionWidgetProperties
class that extendsIWidgetProperties
.The
IWidgetProperties
interface comes with the Xperience by Kentico out-of-box. - Inside the class, define your properties. They will hold the values of your future UI controls:C#SimpleCallToActionWidgetProperties.cs
using System.ComponentModel; using Kentico.PageBuilder.Web.Mvc; using Kentico.Xperience.Admin.Base.FormAnnotations; using Kentico.Xperience.Admin.Websites.FormAnnotations; using TrainingGuides.Web.Features.Shared.OptionProviders; namespace TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction; public class SimpleCallToActionWidgetProperties : IWidgetProperties { public string Text { get; set; } = string.Empty; public string TargetContent { get; set; } = nameof(TargetContentOption.Page); public IEnumerable<WebPageRelatedItem> TargetContentPage { get; set; } = Enumerable.Empty<WebPageRelatedItem>(); public string TargetContentAbsoluteUrl { get; set; } = string.Empty; public bool OpenInNewTab { get; set; } = false; }
Next, we will add the UI form components. To do this, you need to decorate each property with an attribute, based on what component you wish to render. Let’s look at the properties one by one.
Call to action text
Use TextInputComponent
attribute, to render a text input.
[TextInputComponent(
Label = "Call to action text",
ExplanationText = "Add your call to action. Keep it under 30 characters.",
Order = 10)]
public string Text { get; set; } = string.Empty;
Take a closer look at the parameters:
Label
displays before the input field.ExplanationText
displays underneath as an additional description for the editor.Order
determines the placing of the UI control on the form.
Always aim to provide a meaningful label for your UI controls. Based on your use case, consider adding a concise and relevant explanation text as well.
Target content
Editors will use this property to decide the type of target: page or absolute URL.
At the moment, you only need two types, but it is likely you will have to add more options over time (e.g., asset). Choosing a dropdown selector component for this property ensures you can expand the options later, without changing the UI.
Another advantage of a dropdown component is that it supports dynamic options mapping, as described in our earlier guide.
Decorate the TargetContent property with the
DropDownComponent
attribute.Set
Label
,ExplanationText
andOrder
.Fill the dropdown with data.
In this guide, we are using the
DropdownEnumOptionProvider
from earlier in this series, which is the recommended way.If your project doesn’t have it, feel free to pause and follow this guide before coming back to this step. Alternatively, you can skip sub-steps a. through c. and assign dropdown options as a string instead:
Options = "Page;Page\r\nAbsoluteUrl;Absolute URL"
Define a new enumeration in the SimpleCallToActionWidgetProperties.cs file, underneath the
SimpleCallToActionWidgetProperties
class:C#SimpleCallToActionWidgetProperties.cs... public class SimpleCallToActionWidgetProperties : IWidgetProperties { ... } public enum TargetContentOption { [Description("Page")] Page, [Description("Absolute URL")] AbsoluteUrl }
Assign
DataProviderType
, using theDropdownEnumOptionProvider
and your newTargetContentOption
enumeration.Make
TargetContentOption.Page
the default value.
C#[DropDownComponent( Label = "Target content", ExplanationText = "Select what happens when a visitor clicks your button.", DataProviderType = typeof(DropdownEnumOptionProvider<TargetContentOption>), Order = 20)] public string TargetContent { get; set; } = nameof(TargetContentOption.Page);
Target page
- Use a page selector for Target page.
- Set
MaximumPages = 1
to allow editor only pick one page at the time. - Add a visibility condition (
VisibleIfEqualTo
) to ensure, that Target page property only renders if the editor set the Target content toTargetContentOption.Page
.
[WebPageSelectorComponent(
Label = "Target page",
ExplanationText = "Select the page in the tree.",
MaximumPages = 1,
Order = 30)]
[VisibleIfEqualTo(nameof(TargetContent), nameof(TargetContentOption.Page), StringComparison.OrdinalIgnoreCase)]
public IEnumerable<WebPageRelatedItem> TargetContentPage { get; set; } = Enumerable.Empty<WebPageRelatedItem>();
Absolute URL
Like the Call to action text above, make the Absolute URL property a text input.
Add a
VisibleIfEqualTo
visibility condition to only show the field if the editor sets the Target content toTargetContentOption.AbsoluteUrl
.
[TextInputComponent(
Label = "Absolute URL",
ExplanationText = "Add a hyperlink to an external site, or use the product's URL + anchor tag # for referencing an anchor on the page, for example, \"https://your-doma.in/contact-us#form\"",
Order = 40)]
[VisibleIfEqualTo(nameof(TargetContent), nameof(TargetContentOption.AbsoluteUrl), StringComparison.OrdinalIgnoreCase)]
public string TargetContentAbsoluteUrl { get; set; } = string.Empty;
Open in new tab
Our last property is a checkbox component with a bool
value. Set its default value to unchecked (false
). This way, unless the editor changes it, the target will open in the same browser tab.
[CheckBoxComponent(
Label = "Open in new tab",
Order = 50)]
public bool OpenInNewTab { get; set; } = false;
Here is the complete SimpleCallToActionWidgetProperties.cs file for your reference:
using System.ComponentModel;
using Kentico.PageBuilder.Web.Mvc;
using Kentico.Xperience.Admin.Base.FormAnnotations;
using Kentico.Xperience.Admin.Websites.FormAnnotations;
using TrainingGuides.Web.Features.Shared.OptionProviders;
namespace TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction;
public class SimpleCallToActionWidgetProperties : IWidgetProperties
{
[TextInputComponent(
Label = "Call to action text",
ExplanationText = "Add your call to action. Keep it under 30 characters.",
Order = 10)]
public string Text { get; set; } = string.Empty;
[DropDownComponent(
Label = "Target content",
ExplanationText = "Select what happens when a visitor clicks your button.",
DataProviderType = typeof(DropdownEnumOptionProvider<TargetContentOption>),
Order = 20)]
public string TargetContent { get; set; } = nameof(TargetContentOption.Page);
[WebPageSelectorComponent(
Label = "Target page",
ExplanationText = "Select the page in the tree.",
MaximumPages = 1,
Order = 30)]
[VisibleIfEqualTo(nameof(TargetContent), nameof(TargetContentOption.Page), StringComparison.OrdinalIgnoreCase)]
public IEnumerable<WebPageRelatedItem> TargetContentPage { get; set; } = Enumerable.Empty<WebPageRelatedItem>();
[TextInputComponent(
Label = "Absolute URL",
ExplanationText = "Add a hyperlink to an external site, or use the product's URL + anchor tag # for referencing an anchor on the page, for example, \"https://your-doma.in/contact-us#form\"",
Order = 40)]
[VisibleIfEqualTo(nameof(TargetContent), nameof(TargetContentOption.AbsoluteUrl), StringComparison.OrdinalIgnoreCase)]
public string TargetContentAbsoluteUrl { get; set; } = string.Empty;
[CheckBoxComponent(
Label = "Open in new tab",
Order = 50)]
public bool OpenInNewTab { get; set; } = false;
}
public enum TargetContentOption
{
[Description("Page")]
Page,
[Description("Absolute URL")]
AbsoluteUrl
}
Implement widget component
The next step is to define the behavior and visual design of the widget. Let’s implement widget view model, view and view component.
Create view model
- Create a new
SimpleCallToActionWidgetViewModel
class in your SimpleCallToAction folder. - From the UI perspective, the widget needs only three properties:
Text
: call to action text to render (e.g., Contact us)Url
: where navigate after button clickOpenInNewTab
: how to open the target Url
namespace TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction;
public class SimpleCallToActionWidgetViewModel
{
public string Text { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public bool OpenInNewTab { get; set; } = false;
}
Notice that the view model properties don’t exactly mirror the widget properties. The purpose of the widget properties is to gather data from editors. These go through processing in the view component and the view model should include only data that is going to be displayed in the resulting view.
For example, in our case, there is no need for two separate properties for a page and an external link in the view model. The button only needs a target URL to navigate to.
Create view
Add new view file, called SimpleCallToActionWidget.cshtml, in your SimpleCallToAction folder.
Set your
SimpleCallToActionWidgetViewModel
as themodel
.cshtmlSimpleCallToActionWidget.cshtml@using TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction @model SimpleCallToActionWidgetViewModel
Handle the error state of the widget. When either the
Text
,Url
or the wholeModel
are undefined, let’s show a message to tell the editor something is wrong. Remember, you don’t want to show the message to the website visitor:cshtmlSimpleCallToActionWidget.cshtml... <!-- render this part only if the widget is misconfigured --> @if (Model == null || string.IsNullOrWhiteSpace(Model.Text) || string.IsNullOrWhiteSpace(Model.Url)) { <!-- display the message **only** if the page is in edit or preview mode in the Xperience administration--> @if (Context.Kentico().PageBuilder().EditMode || Context.Kentico().Preview().Enabled) { <p> This widget needs some setup. Click the <strong>Configure widget</strong> gear icon in the top right to configure content and design for this widget. </p> } return; }
As you create more widgets in your project, you’ll notice the error handling in widget views is more or less the same. We recommend extracting the functionality, using for example tag helpers (see the SimpleCallToActionWidget view in the finished branch of our repository).
Handle the valid state of the widget and render the button using the values from the model:
cshtmlSimpleCallToActionWidget.cshtml... @if (Model != null) { <div class="text-center"> <a href="@Model.Url" class="btn tg-btn-secondary text-uppercase my-4" target="@(Model.OpenInNewTab ? "_blank" : "")"> @Model.Text </a> </div> }
This code sample takes advantage of classes that exist in our training guides repository, to style the link as a button. Feel free to reuse them or play around with your own styling.
If you are following along, your SimpleCallToActionWidget.cshtml should look like this:
@using TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction
@model SimpleCallToActionWidgetViewModel
@if (Model == null || string.IsNullOrWhiteSpace(Model.Text)
|| string.IsNullOrWhiteSpace(Model.Url))
{
@if (Context.Kentico().PageBuilder().EditMode
|| Context.Kentico().Preview().Enabled)
{
<p>
This widget needs some setup. Click the <strong>Configure widget</strong> gear icon in the top right to configure content and design for this widget.
</p>
}
return;
}
@if (Model != null)
{
<div class="text-center">
<a href="@Model.Url"
class="btn tg-btn-secondary text-uppercase my-4"
target="@(Model.OpenInNewTab ? "_blank" : "")">
@Model.Text
</a>
</div>
}
Create view component
- Add a new SimpleCallToActionWidgetViewComponent.cs file in your SimpleCallToAction folder.
- Create a class with the same name that extends the .NET
ViewComponent
class. - Create an
InvokeAsync
method that- takes
SimpleCallToActionWidgetProperties
as a parameter - creates an instance of your view model and fills it with data, based on the values in
properties
- returns your widget view and passes in the model
C#SimpleCallToActionWidgetViewComponent.csusing Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewComponents; using Kentico.Content.Web.Mvc.Routing; using Kentico.PageBuilder.Web.Mvc; using TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction; namespace TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction; public class SimpleCallToActionWidgetViewComponent : ViewComponent { private readonly IWebPageUrlRetriever webPageUrlRetriever; private readonly IPreferredLanguageRetriever preferredLanguageRetriever; // use dependency injection to access necessary Xperience services public SimpleCallToActionWidgetViewComponent( IWebPageUrlRetriever webPageUrlRetriever, IPreferredLanguageRetriever preferredLanguageRetriever) { this.webPageUrlRetriever = webPageUrlRetriever; this.preferredLanguageRetriever = preferredLanguageRetriever; } public async Task<ViewViewComponentResult> InvokeAsync(SimpleCallToActionWidgetProperties properties) { // figure out what the target URL should be // if the editor sets TargetContent to *Page*, retrieve the page URL, using the GetWebPageUrl function (defined below) // if the editor sets TargetContent to *AbsoluteUrl*, simply read it from the properties parameter string targetUrl = properties.TargetContent switch { nameof(TargetContentOption.Page) => await GetWebPageUrl(properties.TargetContentPage?.FirstOrDefault()) ?? string.Empty, nameof(TargetContentOption.AbsoluteUrl) => properties.TargetContentAbsoluteUrl, _ => string.Empty }; // fill the model with data: // pass along the call to action Text from *properties* // set the *targetUrl* retrieved above // pass along the *OpenInNewTab* from *properties* var model = new SimpleCallToActionWidgetViewModel() { Text = properties.Text, Url = targetUrl, OpenInNewTab = properties?.OpenInNewTab ?? false, }; return View("~/Features/LandingPages/Widgets/SimpleCallToAction/SimpleCallToACtionWidget.cshtml", model); } // retrieve the URL of a web page retrieved from a page selector control private async Task<string?> GetWebPageUrl(WebPageRelatedItem? webPage) => webPage != null ? (await webPageUrlRetriever.Retrieve(webPage.WebPageGuid, preferredLanguageRetriever.Get())) .RelativePath : string.Empty; }
Notice the
GetWebPageUrl
method above.When an editor selects a page using page selector, the value comes to the view component as a
WebPageRelatedItem
object. Because the object doesn’t contain the URL of the page, we useIWebPageUrlRetriever
to retrieve it, and theIPreferredLanguageRetriever
to ensure the URL is accurate for the current site language. - takes
Register the widget
The last piece of the puzzle is to register your new widget so the Xperience recognizes it.
First, your widget needs an identifier. Add is as a constant in your
SimpleCallToActionWidgetViewComponent
class:C#SimpleCallToActionWidgetViewComponent.cs... public class SimpleCallToActionWidgetViewComponent : ViewComponent { // new constant public const string IDENTIFIER = "TrainingGuides.SimpleCallToActionWidget"; private readonly IWebPageUrlRetriever webPageUrlRetriever; private readonly IPreferredLanguageRetriever preferredLanguageRetriever; ... }
Use the
RegisterWidget
assembly attribute to register the widget, using a newly definedIDENTIFIER
.C#SimpleCallToActionWidgetViewComponent.cs... using Kentico.PageBuilder.Web.Mvc; using TrainingGuides.Web.Features.LandingPages.Widgets.CallToAction; using TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction; [assembly: RegisterWidget( identifier: SimpleCallToActionWidgetViewComponent.IDENTIFIER, viewComponentType: typeof(SimpleCallToActionWidgetViewComponent), name: "Simple call to action", propertiesType: typeof(SimpleCallToActionWidgetProperties), Description = "Displays a call to action button.", IconClass = "icon-bubble")] ...
See more information and individual parameters explanation in our documentation page about widget registration.
Place your registration at the top of your SimpleCallToActionWidgetViewComponent.cs file, after the
using
statements.Your full SimpleCallToActionWidgetViewComponent.cs file should look like this:
C#SimpleCallToActionWidgetViewComponent.csusing Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewComponents; using Kentico.Content.Web.Mvc.Routing; using Kentico.PageBuilder.Web.Mvc; using TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction; // register Simple call to action widget [assembly: RegisterWidget( identifier: SimpleCallToActionWidgetViewComponent.IDENTIFIER, viewComponentType: typeof(SimpleCallToActionWidgetViewComponent), name: "Simple call to action", propertiesType: typeof(SimpleCallToActionWidgetProperties), Description = "Displays a call to action button.", IconClass = "icon-bubble")] namespace TrainingGuides.Web.Features.LandingPages.Widgets.SimpleCallToAction; public class SimpleCallToActionWidgetViewComponent : ViewComponent { public const string IDENTIFIER = "TrainingGuides.SimpleCallToActionWidget"; private readonly IWebPageUrlRetriever webPageUrlRetriever; private readonly IPreferredLanguageRetriever preferredLanguageRetriever; public SimpleCallToActionWidgetViewComponent( IWebPageUrlRetriever webPageUrlRetriever, IPreferredLanguageRetriever preferredLanguageRetriever) { this.webPageUrlRetriever = webPageUrlRetriever; this.preferredLanguageRetriever = preferredLanguageRetriever; } public async Task<ViewViewComponentResult> InvokeAsync(SimpleCallToActionWidgetProperties properties) { string targetUrl = properties.TargetContent switch { nameof(TargetContentOption.Page) => await GetWebPageUrl(properties.TargetContentPage?.FirstOrDefault()) ?? string.Empty, nameof(TargetContentOption.AbsoluteUrl) => properties.TargetContentAbsoluteUrl, _ => string.Empty }; var model = new SimpleCallToActionWidgetViewModel() { Text = properties.Text, Url = targetUrl, OpenInNewTab = properties?.OpenInNewTab ?? false, }; return View("~/Features/LandingPages/Widgets/SimpleCallToAction/SimpleCallToACtionWidget.cshtml", model); } private async Task<string?> GetWebPageUrl(WebPageRelatedItem? webPage) => webPage != null ? (await webPageUrlRetriever.Retrieve(webPage.WebPageGuid, preferredLanguageRetriever.Get())) .RelativePath : string.Empty; }
Expose the widget identifier for future reference
There will be cases in your project when you need to reference the widget by its identifier. For example, when restricting widgets allowed in a particular widget zone.
For this purpose we recommend storing references to all your widgets in one place. In our TrainingGuides.Web project, we keep all Widget identifiers as constants inside a static class called ComponentIdentifiers
.
Navigate to ComponentIdentifiers.cs file in the root of the TrainingGuides.Web project.
Locate the
Widgets
class inside theComponentIdentifiers
class.Add a constant referencing
SimpleCallToActionWidgetViewComponent.IDENTIFIER
.C#ComponentIdentifiers.cs... public static class ComponentIdentifiers { public static class Sections { ... } public static class Widgets { ... public const string SIMPLE_CALL_TO_ACTION = SimpleCallToActionWidgetViewComponent.IDENTIFIER; } }
Now you can reference Simple call to action widget anywhere in your project as ComponentIdentifiers.Widgets.SIMPLE_CALL_TO_ACTION
.
For example, if you have followed this guide to implement General section, in the view you can pass something like this into the editable-area-options
:
@new EditableAreaOptions()
{
AllowedWidgets = [ComponentIdentifiers.Widgets.SIMPLE_CALL_TO_ACTION]
}
See the button in action
Now that the work is done, it’s time to build your project and enjoy the fruits of your labor. If your implementation is correct and complete, you should be able to log into the administration interface and add a Simple CTA button to the Home page, as you can see in this video.
If you are not seeing your widget as an option, double-check your widget registration is correct.
If you are getting an error after adding the widget, see the Event log under Development in menu on the left for more information.
What’s next?
The next guide in this series will explain how to create a more advanced widget, demonstrating their true power.