Designing frontends for OPE without wrapping elements
A common scenario I have seen is that a frontend developer or designer implements a design in HTML, CSS, and maybe JS, without worrying about which CMS is used to render it. The code is then copied or moved into Episerver, most often into a Razor view, by an Episerver developer. Then everyone sees the page in On-Page Edit (OPE) and gets a little sad as some of that lovely design is broken. That makes the developers get even sadder as they have to re-do some of the design to work with the extra div elements added by OPE, but at least it will look lovely again.
We would like you to use the HTML structure you want. If you are rendering and handling updates purely in your client-side framework of choice, you should already be able to do this. If you are using Razor, then let us discuss two common design implementations that break, and what we can do with them. But first, let us talk about our two HTML helpers @Html.PropertyFor and @Html.EditAttributes.
HTML-helpers
These are helpers you can use in Razor views, but they are not required if you just want to do on-page editing. See the previous blog posts for more info.
The way @Html.EditAttributes works is that it adds the properties data-epi-property-name and data-epi-use-mvc="True" to the element where it is used, but only in edit mode. It can add more properties if given more parameters. The first attribute enables editing overlays and can be added by yourself to HTML outside Razor. That second attribute tells OPE to use Episerver’s property render controller, that in turn uses .NET’s DisplayFor, to render the property when its data has changed. That partial render replaces the innerHTML of the element with the data-epi-property-name attribute. This becomes an issue for <img> as we will see below.
Similarly, @Html.PropertyFor adds a wrapping element with the same attributes. It uses display templates for the rendered property. The element type can be configured as we will see below.
Scenario 1 – Inline elements
Here we will put a @Html.PropertyFor inside a <p> because we want to edit some strings and the CMS UI needs something to attach to. When previewing it looks fine, but when editing, it drops out to its own line and looses the styling (see the font).
That is because @Html.PropertyFor defaults to wrapping the editable property in a <div>, and that is not a valid child element to a <p>. So, the browser splits the texts into multiple <p>, losing track of any classes or inline styling added to the original <p>. You can see how the font changes in the animation. You can set a custom element type with an anonymous object setting CustomTag or replacing it with @Html.EditAttributes with a suitable element. We will show both ways below:
<!-- scenario 1 markup -->
<p class="article-about scenario">
This article originated in @Html.PropertyFor(x => x.CurrentPage.CountryOfOrigin) and was written by @Html.PropertyFor(x => x.CurrentPage.Author).
</p>
<!-- scenario 1 rendered in OPE -->
<p class="article-about scenario">
This article originated in </p>
<div class="epi-editContainer" data-epi-property-name="CountryOfOrigin" data-epi-use-mvc="True">Sweden</div> and was written by
<div class="author" data-epi-property-name="Author" data-epi-use-mvc="True" data-epi-property-rendersettings="{"cssClass":"author"}">JP</div>.
<p></p>
<!-- scenario 1 rendered in Preview -->
<p class="article-about scenario">
This article originated in Sweden and was written by JP.
</p>
<!-- solution A markup -->
<p class="article-about solution">
This article originated in @Html.PropertyFor(x => x.CurrentPage.CountryOfOrigin, new { CustomTag = "span"}) and was written by <span class="author" @Html.EditAttributes(x => x.CurrentPage.Author)>@Model.CurrentPage.Author</span>.
</p>
<!-- solution A rendered in OPE -->
<p class="article-about solution">
This article originated in <span class="epi-editContainer" data-epi-property-name="CountryOfOrigin" data-epi-use-mvc="True" data-epi-property-rendersettings="{"customTag":"span"}" style="min-height: auto;">Sweden</span> and was written by <span class="author" data-epi-property-name="Author" data-epi-use-mvc="True" style="min-height: 14px; min-width: 18px; display: inline-block;">JP</span>.
</p>
<!-- solution A rendered in Preview -->
<p class="article-about solution">
This article originated in Sweden and was written by <span class="author">JP</span>.
</p>
The main markup difference between the two solutions is that the first span (from PropertyFor) will only be rendered in OPE. Also note that all the editing attributes are only rendered in OPE. For styling with a consistent markup, the second option might be preferred.
You can also use the HTML attributes directly, without the @Html helpers:
<!-- solution B markup -->
<p class="article-about solution">
This article originated in <span data-epi-property-name="CountryOfOrigin" data-epi-use-mvc="True">@Model.CurrentPage.CountryOfOrigin</span> and was written by <span class="author" data-epi-property-name="Author" data-epi-use-mvc="True">@Model.CurrentPage.Author</span>.
</p>
<!-- solution B rendered in OPE -->
<p class="article-about solution">
This article originated in <span data-epi-property-name="CountryOfOrigin" data-epi-use-mvc="True">Sweden</span> and was written by <span class="author" data-epi-property-name="Author" data-epi-use-mvc="True">JP</span>.
</p>
<!-- solution B rendered in Preview -->
<p class="article-about solution">
This article originated in <span data-epi-property-name="CountryOfOrigin" data-epi-use-mvc="True">Sweden</span> and was written by <span class="author" data-epi-property-name="Author" data-epi-use-mvc="True">JP</span>.
</p>
Note that there is no difference between what was rendered in OPE and in Preview. If you are rendering the HTML with a client-side framework you could toggle those attributes yourself. One way to detect that you are in OPE is to render a custom flag when @PageEditing.PageIsInEditMode is true. You can see one example in our Alloy React example repository, where we set a custom data-attribute on the <body> in our _Root.cshtml that we can check for in our React components.
Adding a <span> inside a <p> should not cause any issues, but <img> are trickier...
Scenario 2 – Inline elements and images
This is specific to images, or rather, the <img>. It also not tied to Razor views or HTML helpers, so using the data-epi-property-name (without handling the rendering yourself with data-epi-property-render=”none”, see the documentation) is also affected by this.
In the animation below, we show three different attempts demonstrating what happens with @Html.PropertyFor, @Html.EditAttributes, and because we want to show that it can be worked around, the final polar bear is written with wrapping elements in mind.
Setting the innerHTML when updating a property works for all elements except images. For example, <p> and <span> have text as the innerHTML so when the property has been updated, then setting the innerHTML with the new text will work. When it comes to <img>, there is no valid innerHTML so when it is set it creates invalid markup. Because PropertyFor always adds a wrapping element it will re-render the <img> properly, but the design breaks.
<!-- scenario 2 A markup -->
<div class="article-summary scenario">
@Html.PropertyFor(x => x.CurrentPage.SummaryImage)
<p>@Html.PropertyFor(x => x.CurrentPage.SummaryText)</p>
</div>
<!-- scenario 2 A rendered in OPE -->
<div class="article-summary scenario">
<div class="epi-editContainer" data-epi-property-name="SummaryImage" data-epi-use-mvc="True">
<img src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/spotless-panda.png,,113?epieditmode=False&visitorgroupsByID=undefined" alt="">
</div>
<p></p>
<div class="epi-editContainer" data-epi-property-name="SummaryText" data-epi-use-mvc="True">To the left you can see a "Polar Bear".</div>
<p></p>
</div>
<!-- scenario 2 A rendered in Preview -->
<div class="article-summary scenario">
<img src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/spotless-panda.png,,113?epieditmode=False" alt="">
<p>To the left you can see a "Polar Bear".</p>
</div>
We can try to use EditAttributes instead of PropertyFor, and at first it might seem to work but as can be seen in the second polar bear in the animation, the image does not seem to be replaced when the user updates that property.
<!-- scenario 2 B markup -->
<div class="article-summary scenario">
<img @Html.EditAttributes(x => x.CurrentPage.SummaryImage) src="@Url.ContentUrl(Model.CurrentPage.SummaryImage)" class="image" />
<p @Html.EditAttributes(x => x.CurrentPage.SummaryText)>@Model.CurrentPage.SummaryText</p>
</div>
<!-- scenario 2 B rendered in OPE -->
<div class="article-summary scenario">
<img data-epi-property-name="SummaryImage" data-epi-use-mvc="True" src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/polarbearonice.png,,81?epieditmode=False" class="image">
<img src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/spotless-panda.png,,113?epieditmode=False&visitorgroupsByID=undefined" alt="">
</img>
<p data-epi-property-name="SummaryText" data-epi-use-mvc="True">To the left you can see a "Polar Bear".</p>
</div>
<!-- scenario 2 B rendered in Preview -->
<div class="article-summary scenario">
<img src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/spotless-panda.png,,113?epieditmode=False" class="image">
<p>To the left you can see a "Polar Bear".</p>
</div>
The child <img> in the OPE view is not valid HTML and is not displayed by the browser, causing some confusion for a user as the property did in fact update when it did not look like it.
To be able to change this, we need to make changes to our property renderers and their use of custom display templates. However, it does not seem possible without a breaking change, needing a new major release. If this is an important issue for you that you would like us to focus on, please vote on the bug CMS-9506.
For now, you need to use a wrapping element and write the markup and stylesheet with that in mind. In our example we decided to use a CSS grid, but your solution may vary.
<!-- solution markup -->
<div class="article-summary-grid solution">
<div @Html.EditAttributes(x => x.CurrentPage.SummaryImage) class="left">
<img src="@Url.ContentUrl(Model.CurrentPage.SummaryImage)"/>
</div>
<p @Html.EditAttributes(x => x.CurrentPage.SummaryText) class="right">
@Model.CurrentPage.SummaryText
</p>
</div>
<!-- solution rendered in OPE -->
<div class="article-summary-grid solution">
<div data-epi-property-name="SummaryImage" data-epi-use-mvc="True" class="left">
<img src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/spotless-panda.png,,113?epieditmode=False&visitorgroupsByID=undefined" alt="">
</div>
<p data-epi-property-name="SummaryText" data-epi-use-mvc="True" class="right">
To the left you can see a "Polar Bear".
</p>
</div>
<!-- solution rendered in Preview -->
<div class="article-summary-grid solution">
<div class="left">
<img src="/EPiServer/CMS/Content/contentassets/en/21cd8489b66d40a9b9711df5158be324/spotless-panda.png,,113?epieditmode=False">
</div>
<p class="right">
To the left you can see a "Polar Bear".
</p>
</div>
See the code on our Github (in the branch blog/designing-without-wrapping-elements): https://github.com/episerver/alloy-mvc-template/compare/blog/designing-without-wrapping-elements
We hope this helps you along.
Thanks John for the great post,
Actually I was attracted by the Polar Bear the whole time :D.