Loading...
Area: Episerver Content Delivery API
Applies to versions: 2.6.0 and higher

Content Delivery API and Episerver Forms

Recommendations [hide]

The ContentDeliveryApi.Forms – CD.Forms package supports form rendering in Single-Page Application (SPA) sites with Vue or React. Basically, a form template along with the required resources, such as jQuery, CSS, or external JavaScript, are returned by CD.Forms in JSON format and clients can use that information to render Episerver Forms as in standard Alloy site.

Installation

The CD.Forms package is installed by the NuGet package: EPiServer.ContentDeliveryApi.Forms. After installing the package, form data can be acquired by making requests to endpoints in ContentDeliveryApi.Cms package, the same as for Page and Block.

Request: /api/episerver/v2.0/content/85

Response:

As illustrated by the image, form data is stored in property formModelin JSON result returned. This property contains two pieces of main information:

  • template. The HTML form template.
  • assets. Contains the required resources  (JavaScript and CSS) for Episerver Forms.

FormRenderingService.js

This class is a kind of a library that helps you render forms in your JavaScript framework with the form template and required resources. It is stored under the folder /Scripts/. Basically, this service works in the following order:

  1. Attach the form HTML template to an attach point.
  2. Inject the form’s CSS.
  3. Execute the form’s script in correct order.

 A note here is that two arguments are required:

  • formModel. Data returned from Content Delivery API.
  • element. A DOM node / (node id) that the form template is attached to.

A form’s hosted page

In a standard Alloy site, the FormContainerBlockController can easily resolve the form’s hosted page link from the requested URL. However, in a SPA site, we interact with FormContainerBlockController from the Content Delivery API, so the request context is missed. This causes the form to resolve the hosted page incorrectly and lead to errors such as wrong SubmittedFrom data, and form steps are not displayed because the current page is not correct.

To solve this problem, Content Delivery API checks for a request parameter named currentPageUrl and use it to resolve the current page. When sending a request to get the form model from the Content Delivery API, the client should include this parameter with the value pointing to the current URL.

const parameters = {
                     expand         : '*',
                     currentPageUrl : window.location.pathname
                   };

Music Festival and CD.Forms

The Music Festival  template site demonstrates how to use Content Delivery API to render a form in a SPA site. Music Festival is written in Vue.js, but it is the same if your SPA is using React, Angular, or something else.

Note: This is an example only to demonstrate how you can work with CD.Forms. You can check out the Music Festival code on GitHub.

To make Music Festival work with Episerver Forms, check out the latest code and make some modifications.

FormContainerBlock.vue

First, you need a Vue component responsible for handling form blocks. In this component, create a container node so the form template can be attached to it.

<template>
  <div class="FormContainerBlock">
    <div :id="'formContainer_' + model.contentLink.id"></div>        
  </div>
</template>

Then, on the event mounted, use the FormRenderingService.js to render the form based on the formModel property.

import EpiProperty from '@/Scripts/components/EpiProperty.vue';
import ContentArea from '@/Scripts/components/ContentArea.vue';
import formRenderingService from '@/Scripts/formRenderingService';

export default 
  {
    props: ['model'],
    methods: 
      {
        renderForm: function () 
          {
            var element = document.getElementById('formContainer_' + this.model.contentLink.id);
            formRenderingService.render(this.model.formModel, element);
          }
      },
    components: 
      {
        EpiProperty,
        ContentArea
      },
    mounted() 
      {
        this.renderForm();
      },
    updated: function () 
      {
        this.renderForm();
      }
  };

Lastly, register the component to the Vue engine (in main.js):

import FormContainerBlock from '@/Scripts/components/blocks/FormContainerBlock.vue';
Vue.component('FormContainerBlock', FormContainerBlock);

Modify the Model Mapper

Update the ExtendedContentModelMapper to derived from new ContentModelMapperBase and remove the intercept part in ConfigureContainer method of SiteInitialization.cs.

[ServiceConfiguration(typeof(IContentModelMapper))]
public class ExtendedContentModelMapper : ContentModelMapperBase
  {
    private readonly ServiceAccessor<HttpContextBase> _httpContextAccessor;
    public ExtendedContentModelMapper(IContentTypeRepository contentTypeRepository, 
                                      ReflectionService reflectionService, 
                                      IContentModelReferenceConverter contentModelService,
                                      IUrlResolver urlResolver, 
                                      IEnumerable<IPropertyModelConverter> propertyModelConverters,
                                      IContentVersionRepository contentVersionRepository, 
                                      ContentLoaderService contentLoaderService, 
                                      ServiceAccessor<HttpContextBase> httpContextAccessor) 
                                      : base(contentTypeRepository,
                                             reflectionService,
                                             contentModelService,
                                             urlResolver,
                                             propertyModelConverters, 
                                             contentVersionRepository, 
                                             contentLoaderService)
      {
        _httpContextAccessor = httpContextAccessor;
      }

    public override int Order
      {
        get
          {
            return 110;
          }
      }      

    /// <summary>
    /// Maps an instance of IContent to ContentApiModel and additionally add info about existing languages
    /// </summary>
    public override ContentApiModel TransformContent(IContent content, bool excludePersonalizedContent, string expand)
      {
        var contentModel = base.TransformContent(content, excludePersonalizedContent, expand);
        if (contentModel == null)
          {
            return null;
          }
        contentModel.Properties = contentModel.Properties.Select(FlattenProperty).ToDictionary(x=>x.Key, x=>x.Value);
        if (contentModel.ParentLink != null)
          {
            contentModel.ParentLink.Url = ResolveUrl(content.ParentLink, content.LanguageBranch());
          }
        contentModel.Properties.Add("parentUrl", contentModel.ParentLink?.Url);            
        return contentModel;
      }
    protected override string ResolveUrl(ContentReference contentLink, string language)
      {
        return _urlResolver.GetUrl(contentLink, language, new UrlResolverArguments
          {
            ContextMode = GetContextMode()
          });
      }    

    private static KeyValuePair<string, object> FlattenProperty(KeyValuePair<string, object> property)
      {
        return new KeyValuePair<string, object>(property.Key, FlattenPropertyValue(property.Value));
      }
    /// <summary>
    /// Extract the actual value from the property model.
    /// In our simple Vue app we are only interested about the property values,
    /// flattening the property model will simplify our client side components.
    /// </summary>
    private static object FlattenPropertyValue(object propertyValue)
      {
        switch (propertyValue)
          {
            case ContentAreaPropertyModel propertyModel : return propertyModel?.ExpandedValue?.Select(x=>ConvertContentAreaItem(x, propertyModel));
            case BuyTicketBlockPropertyModel propertyModel : return propertyModel.Value;
            case PropertyModel<string, PropertyString> propertyModel : return propertyModel.Value;
            case PropertyModel<string, PropertyUrl> propertyModel : return propertyModel.Value;
            case PropertyModel<DateTime?, PropertyDate> propertyModel : return propertyModel.Value;
            case PropertyModel<bool?, PropertyBoolean> propertyModel : return propertyModel.Value;
            case PropertyModel<string, PropertyLongString> propertyModel : return propertyModel.Value;
            case PropertyModel<string, PropertyXhtmlString> propertyModel : return propertyModel.Value;
            case CategoryPropertyModel propertyModel : return propertyModel.Value;
            default : return propertyValue;
          }
      }
    /// <summary>
    /// We need to extend the model for content areas with available display options so our component will get a correct css class
    /// see how it's used in Assets/Scripts/components/ContentArea.vue
    /// </summary>
    private static object ConvertContentAreaItem(ContentApiModel contentApiModel, ContentAreaPropertyModel propertyModel)
      {
        var contentModelDisplayOption = propertyModel.Value.FirstOrDefault(x => x.ContentLink.Id == contentApiModel.ContentLink.Id)?.DisplayOption;
        contentApiModel.Properties.Add("displayOption", contentModelDisplayOption);
        return contentApiModel;
      }

    /// <summary>
    /// The "epieditmode" querystring parameter is added to URLs by Episerver as a way to keep track of what context is currently active.
    /// If there is no "epieditmode" parameter we're in regular view mode.
    /// If the "epieditmode" parameter has value "True" we're in edit mode.
    /// If the "epieditmode" parameter has value "False" we're in preview mode.
    /// All of these different modes will resolve to different URLs for the same content.
    /// </summary>
    private ContextMode GetContextMode()
      {
        var httpCtx = _httpContextAccessor();
        if (httpCtx == null || httpCtx.Request == null || httpCtx.Request.QueryString[PageEditing.EpiEditMode] == null)
          {
            return ContextMode.Default;
          }
        if (bool.TryParse(httpCtx.Request.QueryString[PageEditing.EpiEditMode], out bool editMode))
          {
            return editMode ? ContextMode.Edit : ContextMode.Preview;
          }
        return ContextMode.Undefined;
      }

    public override bool CanHandle<T>(T content)
      {
        return content is IContent;
      }
  }
public void ConfigureContainer(ServiceConfigurationContext context)
  {
    DependencyResolver.SetResolver(new StructureMapDependencyResolver(context.StructureMap()));
    context.Services.AddTransient<IPropertyModelConverter, BuyTicketBlockPropertyModelConverter>();
    context.Services.AddTransient<RoutingEventHandler, CustomContentApiRoutingEventHandler>();
    context.Services.AddSingleton<ContentLoaderService, CustomContentLoaderService>();
    context.Services.AddSingleton<UrlResolverService, CustomUrlResolverService>();

    // set minimumRoles to empty to allow anonymous calls (for visitors to view site in view mode)
    context.Services.Configure<ContentApiConfiguration>(config =>
      {
        config.Default().SetMinimumRoles(string.Empty);
      });
  }

Edit api.js

By default, Music festival uses friendly URLs to acquire content from Content Delivery API and do not send Accept-Language header. However, in the case of forms, send the language header to retrieve the correct version if our forms have multiple languages.

getContentByContentLink: (contentLink, parameters, headers) => 
  {
    if (headers == null) {headers = { 'Accept-Language': '' };}
    return get(`${applicationPath}api/episerver/v2.0/`, `content/${contentLink}`, parameters, headers);
  }

Add currentPageUrl parameter in epiDataModel.js

Modify epiDataModel.js so it includes an extra currentPageUrl parameter when sending a request to retrieve content.

const parameters = {
                     expand         : '*',
                     currentPageUrl : window.location.pathname
                   };

Rebuild client resources

Rebuild the client resources of the Music Festival site for the changes to take effect. Open a command line at the project's root folder and type npm run build to run the build script.

After the build process finishes, create a form in edit view, drag and drop it to any page and the form displays automatically.

Note: Music Festival’s on-page edit view does not support Episerver Forms currently, so use the all properties view to create and edit a form.

Edit Form in all properties view

Drag a form to page content area

The form should automatically be display in view mode.

Do you find this information helpful? Please log in to provide feedback.

Last updated: Jul 01, 2019

Recommendations [hide]