Glass Edit Frame – Immediately Invoking Wrapper

tl;dr; Custom HTML Helper to wrap Glass Edit Frame and JS code to immediately open the modal dialog for editing

As I’m sure most of you are aware, I 😍 Glass Mapper. So much so that I struggle with the native Sitecore API 😂 

Version 4 of the framework added an amazing feature which allows you to very simply and quickly create/bind to Edit Frames purely from code. Normally this is a bit of an annoying and long winded processing: switch over to the core database, create an item with names of the fields, serialize/sync to source control, do this for every combination of fields you have, hook it up to EditFrame code that until recently did not work in native Sitecore MVC 😆

I’m sure you’ve been using this feature, it’s super simple from code to add an EditFrame wherever you need and the best part is it’s bound against your strongly typed model:

@using (BeginEditFrame(Model.Page, "Edit Metadata", x => x.DisplayInMenu, x => x.Closed))
{
  <div>Let's add an Edit Frame around our rendering</div>
}

If you need to add another field then simply add it to the list of fields and Glass handles everything for you.

Due to the simplicity, we use these a lot. It’s the only way to edit certain types of fields in the Experience Editor (such as multilist fields, checkboxes, treelists etc) without using Rendering Parameters, which in itself would cause other issues such as not being able to personalise, sharing them in Final Layout renderings, translate them or put them through workflow.

Our frontend team did an amazing job to create some custom buttons for the Edit Frames, using FontAwesome to provide some iconography. We just add code like so wherever we need to add in a EditFrame:

@if (Sitecore.Context.PageMode.IsExperienceEditor)
{
  <div class="widget__properties">
    @using (Html.Glass().BeginEditFrame(Model, "Edit Properties", x => x.CssClass, x => x.BackgroundImage))
    {
      <p class="properties-button">Edit Properties</p>
    }
  </div>
}

This renders something like this:

Custom Edit Frame Wrapper

The problem is the code became a bit repetitive. We need something a bit simpler so we had consistent usage throughout the entire project without all the extra code bulk. I was also getting a bit annoyed by the pointless extra click of selecting the button then the cubes…

So here comes the code part. The end result is probably best demonstrated with a video:

You can see a static CodePen demo here: https://codepen.io/jammykam/pen/PKWRyz

What have we done? We’ve created an HTML Extension method which in turn wraps the Glass Edit Frame code. This allows us to carry out the PageMode check and add in our own wrapper markup. This also allows us to bind a custom JavaScript handler to the button to trigger the code which opens the Edit Frame modal.

<div class="scLooseFrameZone scEnabledChrome" sc_item="sitecore://master/{CC718A99-7B67-47E6-9138-65A08A95EC21}?lang=en&amp;ver=1" sc-part-of="editframe">
  <span class="scChromeData">
    {"commands":
     [{"click":"javascript:Sitecore.PageModes.PageEditor.postRequest('webedit:fieldeditor(command={86D83C17-F6CA-4B36-99BF-6965BCB72201},fields=Title|Description|Image,id={CC718A99-7B67-47E6-9138-65A08A95EC21})',null,false)",
        "header":"Edit Fields", "icon":"/temp/iconcache/people/16x16/cubes_blue.png",
        "disabledIcon":"/temp/cubes_blue_disabled16x16.png", "isDivider":false,
        "tooltip":"Edit the following fields: Title, Description, Image", "type":null}],
    "contextItemUri":"sitecore://master/{CC718A99-7B67-47E6-9138-65A08A95EC21}?lang=en&amp;ver=1",
    "custom":{},
    "displayName":" Edit Properties",
    "expandedDisplayName":""}
  </span>
  <button class="btn-editframe" title="Edit Properties">
    <i class="fa fa-edit"></i> Edit Properties
  </button>
</div>

We’re not doing anything magical, just immediately triggering the code that Sitecore would when you click the button from the webedit ribbon. The important line of code is hidden away in the scChromeData span and we just need to parse this and trigger the click command ourselves (line 4 above).

Usage

Include the JS and CSS files in Experience Editor mode and then use the HTML Helper to render the button style you require.

@Html.EditFrame(item, x => x.Link, x => x.Image) small button
small button
@Html.EditFrame(item, true, x => x.Link, x => x.Image) default text + button
default text + button
@Html.EditFrame(item, “fa-wrench”, x => x.Link, x => x.Image) small button, custom icon
small button, custom icon
@Html.EditFrame(item, “Override Text”, “”, x => x.Image) override text, default button
override text, default button
@Html.EditFrame(item, “Override Text”, “fa-cogs”, x => x.Setting) override text + button
override text + button
@Html.EditFrame(item, “Edit Background”, showTitle:false, “fa-image”, x => x.Image) ustom button, no text, set custom hover title
custom button, no text, set custom hover title

The buttons use FontAwesome Icons, you need to include a reference to the css file then you can pass in any valid class name to override the default: http://fontawesome.io/icons/
 
If you want to render multiple buttons then you need to use this IDisposable wrapper allowing you to group multiple buttons. This allows us to use some different styling so the buttons appear inline rather than stacking up on top of each other. It’s also possible to pass a CSS class to this wrapper if required (to set  different placement of the button, for example).

@using (Html.EditFrameWrapper())
{
  @Html.EditFrame(item, "fa-cogs", x => x.Setting)
  @Html.EditFrame(item, "fa-wrench", x => x.Configuration)
  @Html.EditFrame(item, "fa-image", x => x.Image)
}
Multiple Buttons

The above is all that you need. Nothing else. No using statements for single buttons. As you can see, really clean usage with the added benefit of allowing us to bind our custom JavaScript handler.

Code

Keep in mind that this is sample code that works for us. Our project uses Foundation 5 as a base, I’ve tried to extract out the styles so it should be framework agnostic. But, ya know, update the styles as required.

@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css&#39;);
/* Glass Edit Frame Extensions */
.c-editframe {
box-shadow: 3px 3px 6px rgba(0,0,0,0.275);
position: absolute;
right: .5rem;
top: .5rem;
}
.c-editframe .btn-editframe {
background-color: #fafafa;
border-radius: 3px;
border: solid 1px #ddd;
box-shadow: 3px 3px 6px rgba(0,0,0,0.275);
color: #2b2b2b;
display: block;
font-size: 0.8em;
font-family: Arial, sans-serif;
height: auto;
margin: 0;
padding: .5em .7em;
}
.c-editframe .btn-editframe:hover {
background-color: #ddd;
}
.c-editframe-wrapper {
position: absolute;
right: .5rem;
top: .5rem;
}
.c-editframe-wrapper .c-editframe {
float: left;
margin-left: .3em;
position: relative;
right: initial;
top: initial;
}

/**
* Custom EditFrame Handler *
* This code hooks into a custom Glass Edit Frame wrapper and ensures
* the dialog opens immediately rather than using the webedit ribbon
*/
if (typeof Sitecore !== typeof undefined) {
Sitecore.PageModes.PageEditor.glassEditFrameHandler = function() {
$('.js-btn-editframe').not('.js-ef_attached')
.on('click', function (e) {
var data = JSON.parse($(this).prev('.scChromeData').html());
eval(data.commands[0].click);
e.stopPropagation();
})
.addClass('js-ef_attached');
};
// attach handlers on page load
Sitecore.PageModes.PageEditor.onLoadComplete.observe(Sitecore.PageModes.PageEditor.glassEditFrameHandler);
// attach handlers when new renderings are inserted
Sitecore.PageModes.ChromeTypes.Placeholder = Sitecore.PageModes.ChromeTypes.Placeholder.extend({
insertRendering: function(data, openProperties) {
this.base(data, openProperties);
Sitecore.PageModes.PageEditor.glassEditFrameHandler();
}
},
{
emptyLookFillerCssClass: Sitecore.PageModes.ChromeTypes.Placeholder.emptyLookFillerCssClass,
getDefaultAjaxOptions: Sitecore.PageModes.ChromeTypes.Placeholder.getDefaultAjaxOptions
});
}

view raw
experience-editor.js
hosted with ❤ by GitHub

using System;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;
using Glass.Mapper.Sc;
namespace MyProject.Custom.HtmlHelpers
{
public static class GlassEditFrameExtensions
{
/// <summary>
/// Outputs EditFrame with default icon only
/// </summary>
/// <param name="model">Glass Model</param>
/// <param name="fields">Fields to edit</param>
/// <returns>Renders Edit Frame</returns>
public static IHtmlString EditFrame<T>(this HtmlHelper helper, T model, params Expression<Func<T, object>>[] fields) where T : class
{
return helper.EditFrame(model, null, false, null, fields);
}
/// <summary>
/// Outputs EditFrame with default icon and allows text to be hidden
/// </summary>
/// <param name="model">Glass Model</param>
/// <param name="showTitle">Show text on button?</param>
/// <param name="fields">Fields to edit</param>
/// <returns>Renders Edit Frame</returns>
public static IHtmlString EditFrame<T>(this HtmlHelper helper, T model, bool showTitle, params Expression<Func<T, object>>[] fields) where T : class
{
return helper.EditFrame(model, null, showTitle, null, fields);
}
/// <summary>
/// Outputs EditFrame and allows button icon to be overridden. Icon only, no text.
/// </summary>
/// <param name="model">Glass Model</param>
/// <param name="buttonClass">CSS class for button to override icon</param>
/// <param name="fields">Fields to edit</param>
/// <returns>Renders Edit Frame</returns>
public static IHtmlString EditFrame<T>(this HtmlHelper helper, T model, string buttonClass, params Expression<Func<T, object>>[] fields) where T : class
{
return helper.EditFrame(model, null, false, buttonClass, fields);
}
/// <summary>
/// Outputs EditFrame and allows button text and icon to be overridden.
/// </summary>
/// <param name="model">Glass Model</param>
/// <param name="title">Button text</param>
/// <param name="buttonClass">CSS class for button to override icon</param>
/// <param name="fields">Fields to edit</param>
/// <returns>Renders Edit Frame</returns>
public static IHtmlString EditFrame<T>(this HtmlHelper helper, T model, string title, string buttonClass, params Expression<Func<T, object>>[] fields) where T : class
{
return helper.EditFrame(model, title, true, buttonClass, fields);
}
/// <summary>
/// Outputs EditFrame and allows button text and icon to be overridden, or text to be hidden.
/// </summary>
/// <param name="model">Glass Model</param>
/// <param name="title">Button text</param>
/// <param name="showTitle">Show text on button?</param>
/// <param name="buttonClass">CSS class for button to override icon</param>
/// <param name="fields">Fields to edit</param>
/// <returns>Renders Edit Frame</returns>
public static IHtmlString EditFrame<T>(this HtmlHelper helper, T model, string title, bool showTitle, string buttonClass, params Expression<Func<T, object>>[] fields) where T : class
{
if (!Sitecore.Context.PageMode.IsExperienceEditor)
return new HtmlString("");
var writer = helper.ViewContext.Writer;
var sitecoreContext = SitecoreContextFactory.Default.GetSitecoreContext();
// set up defaults
var tooltip = string.IsNullOrEmpty(title) ? "Edit Properties" : title;
title = showTitle ? $" {tooltip}" : string.Empty;
buttonClass = string.IsNullOrEmpty(buttonClass) ? "fa-edit" : buttonClass;
// render the wrapped editframe
writer.Write("<div class=\"c-editframe\">");
using(sitecoreContext.GlassHtml.EditFrame(model, title, writer, fields))
{
writer.Write($"<button class=\"btn-editframe js-btn-editframe\" title=\"{tooltip}\"><i class=\"fa {buttonClass}\"></i>{title}</button>");
}
writer.Write("</div>");
return new HtmlString("");
}
}
}

view raw
GlassEditFrame.cs
hosted with ❤ by GitHub

using System;
using System.IO;
using System.Web.Mvc;
namespace MyProject.Custom.HtmlHelpers
{
public static class GlassEditFrameWrapperExtensions
{
public static IDisposable EditFrameWrapper(this HtmlHelper helper)
{
return EditFrameWrapper(helper, string.Empty);
}
public static IDisposable EditFrameWrapper(this HtmlHelper helper, string cssClass)
{
if (!Sitecore.Context.PageMode.IsExperienceEditor)
return new EmptyWrapper();
var writer = helper.ViewContext.Writer;
writer.Write($"<div class=\"c-editframe-wrapper {cssClass}\">");
return new CloseWrapper(writer);
}
private class CloseWrapper : IDisposable
{
private readonly TextWriter _writer;
public CloseWrapper(TextWriter writer)
{
_writer = writer;
}
public void Dispose()
{
_writer.Write("</div>");
}
}
private class EmptyWrapper : IDisposable
{
public void Dispose() { }
}
}
}

There’s not much to it but it took me a while to figure out all the pieces to make this work. I didn’t want to bog the post down with explanation of all the bits of the code, but guess why I wrote my last post about extending Sitecore JavaScript. Hopefully it makes sense but feel free to reach out to me or leave a comment.

2 comments

  1. Ben · November 29, 2018

    This looks perfect but I’m getting “SitecoreContextFactory” does not exist in the current context. (GlassEditFrame.cs Line 74)
    I’m using the latest version of Glass and wondering if the methods have moved somewhere? Do you know how I can fix this?

    • OtherBen · November 30, 2018

      I got it going by replacing the SitecoreContextFactory with:
      var sitecoreService = new SitecoreService(Sitecore.Context.Database);
      var glassHtml = new GlassHtml(sitecoreService);

      Then use glassHtml directly:
      using (glassHtml.EditFrame(model, title, writer, fields))

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s