Taking control of client-side rendering in OPE (Beta) (CMS UI 10.12.0)
You can read more about how you enable Beta features in Fredrik Tjärnberg’s blog post.
With CMS UI 10.12.0 we’re including some options to better support the On-Page Editing (OPE) experience for websites that want to handle the view on the client-side. This could be using a JavaScript framework such as React or Angular. The main issue is about controlling the DOM and Ted Nyberg explains it well in his blog. Many projects work around it by simply asking the editors to use the Properties Mode instead, which is why we previously released "Sticky View Mode" in CMS UI 10.11.0.
Here we’ll go through four things you can use to improve the OPE experience while using Angular, React, or any other JavaScript framework. You can read more in the Editing documentation.
Three HTML attributes for editing (see "Telling OPE that you’ll take over the rendering"):
- data-epi-property-name="YourProperty"
- data-epi-property-render="none"
- data-epi-property-edittype="floating" (usually default, but needed for string properties)
And the global JavaScript object (see "Knowing when to re-render"):
- epi.subscribe("beta/contentSaved", callback)
Telling OPE that you’ll take over the rendering
To stop the CMS UI from replacing the DOM when an editor changes the value of a property, add the HTML attribute data-epi-property-render="none" to the DOM element that's marked for editing with @Html.EditAttributes(m => m.YourProperty) or data-epi-property-name="YourProperty".
If you’re editing a string-type you can add the data-epi-property-edittype="floating" to get a dialog with the text, instead of contenteditable replacing the DOM as the user types.
Knowing when to re-render
Now that OPE won't replace the DOM as the user changes property values you’ll need to do this yourself. But how do you know when when or what to change?
For this we’ll use the little known "epi" object that allows us to communicate between the CMS UI context and the view. Whenever a save happens we will publish the details on a topic called "beta/contentSaved". You can subscribe to this topic to know when and what to change in your view.
If you based your site on Alloy you probably have this object already. To get it you need have the attribute [RequireClientResources] on your controller, or just inherit from PageController or ContentController as they'll include it. Then you’ll need to require the resources in your razor view where you include any other scripts, probably in _Root.cshtml, using: @Html.RequiredClientResources("Footer")
A simple string example (without JS)
Let’s see how things look without JavaScript. When it comes to string property types they’re changing the DOM as you type. Besides the problems for client-side frameworks, it makes editing awkward in certain scenarios such as the placeholder text in input-elements.
To achieve this we’ll add a property to the model. I’m using Alloy and the Search page, so in Models/Pages/SearchPage.cs I’m adding:
public virtual string SearchPlaceholderText { get; set; }
And in its Razor-view, which for me is Views/SearchPage/Index.cshtml, add this to force a reload:
@Html.FullRefreshPropertiesMetaData(new[] { "SearchPlaceholderText" })
And change the input element from this:
<input type="text" tabindex="1" name="q" value="@Model.SearchedQuery" />
To this:
<input type="text" tabindex="1" name="q" value="@Model.SearchedQuery" placeholder="@Model.CurrentPage.SearchPlaceholderText" data-epi-property-name="SearchPlaceholderText" data-epi-property-edittype="floating" />
See the code in the links below.
A simple example in AngularJS
Let’s use Alloy, but change the page image for Product pages so that we're showing the editor some extra info about the image. This could be useful for sites that have more image metadata or other data that depend on the selected image.
This is what we're going for:
Originally, the view looks like this:
<div @Html.EditAttributes(x => x.CurrentPage.PageImage)>
<img src="@Url.ContentUrl(Model.CurrentPage.PageImage)"/>
</div>
We're changing it to this:
<div data-epi-property-name="PageImage" data-epi-property-render="none">
<img ng-src="{{ctrl.pageImageUrl}}" alt="{{ctrl.pageImageName}}"/>
@if (PageEditing.PageIsInEditMode)
{
<pre>Drag an image here to test OPE with Angular!<br/>Image name: {{ctrl.pageImageName}}</pre>
}
</div>
Note that @Html.EditAttributes() is replaced with data-epi-property-name. This enables the content overlay and allows the editor to change the value of that property.
Also note the data-epi-property-render that's telling the renderer that we'll be handling things for this element!
To get some start values, we're copying parts of the model to a global object that our Angular controller can initialize with.
<script>
window.pageModel = {
contentLink: "@Model.CurrentPage.ContentLink",
pageImage: "@Model.CurrentPage.PageImage"
};
</script>
The code on Github has more functionality and comments explaining them, so here I’ll just outline the important part: how to update the values when the page is being edited.
You could work against a service and/or have this code in the Razor view, but it’s tricky to communicate between the global scope and Angular. So I chose to have this inside the controller as it's easier to demonstrate.
var vm = this;
vm.model = $window.pageModel || {};
// Helper method to get image URL and name through XHR and update pageImage properties.
function updatePageImage(contentLink) { … }
// Wait until the window has loaded, and then hook up our property update logic.
$window.addEventListener("load", function () {
// Subscribe to the contentSaved topic. Now the fun starts!
$window.epi.subscribe("beta/contentSaved", function (details) {
// Go through all the saved properties. Usually it's only one.
details.properties.forEach(function (property) {
switch (property.name) {
// Handle the page image property differently, because we only get the content reference but need more data for the page.
case "pageImage":
updatePageImage(property.value);
break;
// Otherwise just update the value on the model.
default:
vm.model[property.name] = property.value;
break;
}
});
});
});
The documentation has an example of the object you might get from the event, but below is the interesting part:
{
"contentLink":"6_164", // The content reference to this page
"properties":[
{
"name":"pageImage", // The property that changed
"value":"59" // The new value. Content references will need an extra lookup to get what you want, but string and other properties can be usable directly.
}
]
}
See the code in the links below.
A complex example in React
We have an example of how to rewrite the PageListBlock in React but it's a little big for this blog post. The basic usage of the data-attributes and listening to the “beta/contentSaved” message is the same as in the Angular example above. See the code in the links below.
Code on Github
Please contribute to these repo's to help each other how to work with OPE and your favourite JavaScript framework!
A simple string example (without JS): https://github.com/seriema/AlloyNoJS
A simple example in AngularJS: https://github.com/seriema/AlloyAngularJS
A complex example in React: https://github.com/seriema/AlloyReact
Looks awsome!
Awesome, I am eager to bring VueJS to this.
VueJS is really great! I believe that Vue is a viable alternative between Angular JS and React. It’s flexible, and simply beautiful both in terms of internal architecture and API.
Looks great @John, I'm a fan of Angular and we now have correct guide for it.
Thanks John! I can't wait to see it's possible to apply either ReactJS or VueJS on our CMS.
Glad to hear it! The architecture in React and VueJS is very similar so the React sample should help. If you create a "AlloyVue" repo we'd love to include it here!
Cool! I'll try it out with the next hot JS framework for sure.
Nice post!
I'm trying out an architechture where I provide all data via an web API and one component per page, in React. I'm using React Router to create a true SPA. It works okay-ish, except for one thing. I have been able to get the blue border around my properties on the initial page load, but when I enter another page in my SPA, the blue borders disappear. Is there a way to tell Dojo to hook up the new properties?
I could go into more detail if needed.
@Andreas, yes we have seen that as well and it's part of a set of problems we're working towards solving (we presented them at the Episerver events in Denmark and Sweden this past month but haven't made any online posts on it, yet). We are thinking on how to solve this particular issue. If you can provide some code, maybe fork and create a branch from the AlloyReact repo, we could use that as a test case to see that we're solving a real problem. It'd be a great help!
@John, sorry for not replying earlier, but time came in the way.
There's just so much code in Alloy that wouldn't be relevant in my case. I continued on my own project: https://github.com/AndreasJilvero/Episerver.SPA.
As a end user it works just fine, but as a editor, OPE fails. I use React Router to dynamically create routes and pages, and when I change route I need a way to tell Dojo to re-initialize its OPE.
If you try the my solution... sorry for the lack of a CSS file ;)
@Andreas, thank you! That's a great help! The problems with OPE overlays is the next thing we'll be looking into. Your repo will be a nice benchmark for us to verify against. :)